昨天聊了 callback 与 Promise,是如何过关斩将,不断克服障碍走到 ES6。
然而只要程序规模不断扩张,就永远会有更高阶的需求产生,让我们继续来看,Promise 之後发生了什麽故事吧!
ES6 Promise 已经是满不错的设计,只是它长得还是太像非同步了(?),如果我有资料仰赖非同步取回,就会变成同步跟非同步两个大区块,但 ES7 新登场的 async/await,开始渐渐模糊了同步与非同步的界线。
async/await 是 ES7 版本的一个 Promise 语法糖,透过 async
跟 await
两个关键字,可以将原本执行多行 Promise 程序简化成一行,并且使用方式非常贴近一般的同步程序码,大幅提高程序的可读性。
async
关键字放在 function 的前面,代表「宣告一个非同步的函式」await
关键字放在呼叫 async function 的前面,代表「呼叫并等待这个非同步函式」async
跟 await
是成对出现的async function 其实也是一种物件种类,有兴趣可以查看 Mozilla MDN
// 用 setTimeout 模拟一个要等 3 秒的 Promise
const wait3Seconds = async (x) => {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 3000);
});
}
// 函式先 await 再赋值
const a = await wait3Seconds(1);
const b = await wait3Seconds(2);
const sum = a + b;
console.log(sum); // 3
// 函式先赋值再 await
const c = wait3Seconds(1);
const d = wait3Seconds(2);
const sum2 = await c + await d;
console.log(sum2); // 3
有注意到上面的例子中,await 的摆放位置不同吗?可是执行出来的 sum 跟 sum2 结果都一样耶,所以 await 放哪很重要吗?
好奇的话不妨复制贴到 console 跑跑看,记得分两段跑,1~14 行先跑,看完结果再跑 17~20 行。
先公布答案:
第一段要等 6 秒才会有结果,而第二段只要等 3 秒。为什麽呢?
新手如果不熟,不妨这样记忆:看到 await
代表要等。
第一段 (函式先 await
再赋值):
await
所以要等,setTimeout
被启动开始倒数a
await
所以要等,setTimeout
被启动开始倒数b
第二段 (函式先赋值再 await
):
await
所以不等,把 Promise 物件赋值给 c
,setTimeout
被启动开始倒数await
所以不等,把 Promise 物件赋值给 d
,setTimeout
被启动开始倒数await
所以要等 c
完成await
所以要等 d
完成,但刚刚那 3 秒已经让 d
也完成了,所以相加之後印出另一个值得思考的点是,因为 await
的顺序不同,导致 a
、b
这两个变数存的是 number
,而 c
、d
则存了 Promise
物件,有兴趣不妨印出来看看。
简化了 Promise 复杂的结构,让非同步函式可以变得像是同步一样,不仅可读性更高,在 error handling 的方面,也可以使用既有的 try catch
来解决,某种程度上也让新手更容易上手。
如同昨天 Promise 的例子,同步/非同步的界线非常鲜明,要改动常常要顾虑这一行到底是同步还非同步,但改用 async/await 之後,看起来全都像是同步程序码:
const doQueryWallet = async () => {
// 这里都跟原本一样,只是多了 async
};
const doOrder = async () => {
// 这里都跟原本一样,只是多了 async
};
const wallet = await doQueryWallet();
if (wallet.balance > 0) {
const order = await doOrder();
if (order.status === 'ok') {
console.log('下单成功');
} else {
console.log('下单失败');
}
} else {
console.log('余额不足');
}
当然 error handling 的部分,也可以用熟悉的 try catch
包起来:
try {
const wallet = await doQueryWallet();
if (wallet.balance > 0) {
const order = await doOrder();
if (order.status === 'ok') {
console.log('下单成功');
} else {
console.log('下单失败');
}
} else {
console.log('余额不足');
}
} catch (err) {
console.error(err);
}
以上讲的 case,大部分都还是基於「由上而下」的顺序,也就是我们很熟悉同步的程序码顺序,一个一个由上而下执行,第二行绝对会等第一行结束才执行。
非同步有以下三种常见的顺序,可以透过 async/await 及 Promise 简单达到以下效果:
这是我们最常见的顺序,A 执行完换 B,B执行完换 C,後者不能早於前者,通常是因为後者依赖於前者的资料。
比如:一定要先查完帐户余额,确定有余额才能够下单。
const sequence = async () => {
const output1 = await a();
const output2 = await b();
const output3 = await c();
return `sequence is done ${output1} ${output2} ${output3}`;
}
成功的情况
a
|--------|
b
|--------|
c
|--------|
非同步结束
|----------
拒绝的情况
a
|--------|
b
|--------X
非同步结束(throw error)
|--------
这是一个比较有效率的执行方式,不管顺序,把所有非同步程序都撒出去执行,等全部都完成(fulfilled
)再一次告诉我。通常代表这几个非同步函式没有互相依赖的资料,可以同时执行。
比如:查询店家评论、查询我的优惠券、查询帐户余额,三者没有互相依赖的资料,但是会出现在同一个页面,因此把三个函式都发出去,全部回来再一次做接下来的动作(比如把 loading 图示关掉)。
const parallel = async () => {
const [output1, output2, output3] = await Promise.all([a(), b(), c()]);
return `parallel is done ${output1} ${output2} ${output3}`;
}
成功的情况
a
|-----|
b
|--------------|
c
|--------|
非同步结束,取得回传结果
|----------
拒绝的情况
a
|-----X
b
|--------------|
c
|--------|
非同步结束,取得回传错误
|----------
注意,成功与失败的回传值是不同的,成功时把结果包成一个 array 回传,里头的顺序即
Promise.all
里面的顺序。而拒绝则是任何一个 Promise 拒绝(reject
)就会立刻回传最早拒绝的结果。
注意,Promise 是不可打断的,发出去的函式跟泼出去的水一样,唯一能做的,就是忽略回传值,所以即便上图 a 最早就被拒绝了,b 跟 c 还是会把他们该做的事情在背景跑完,只是我们不会收到结果就是了。
这算是 parallel 的一个变种(?),也是把所有非同步程序都撒出去执行,但就像赛跑一样,只要任何一个完成(fulfilled
)或拒绝(reject
),就立刻回传告诉我,其他未完成的则直接忽略,比较是运用在较特殊的情境。
比如:可以帮 request 设定一个 timeout 的时间限制,放一个 setTimout 10 秒就会 reject
的 Promise 进去 Promise.race
,就会强制在 10 秒之内得到结果。
成功的情况
a
|-----|
b
|--------------|
c
|--------|
非同步结束,取得回传结果
|----------
拒绝的情况
a
|-----X
b
|--------------|
c
|--------|
非同步结束,取得回传错误
|----------
const race = async () => {
const output1 = await Promise.race([a(), b(), c()]);
return `race is done ${output1}`;
}
非同步很复杂,但也就是因为很复杂,可以玩的花样更多,许多复杂的需求其实都是建构在非同步程序中,否则我们的程序就永远只能同步一行一行执行,似乎也少了一些醍醐味。
无论如何看待
我终将完成我的
承诺
JAVACRIPT.INFO: Promises, async/await
MDN - Promise.all
MDN - Promise.race
Nest 在大多数情况下是采用 单例模式 (Singleton pattern) 来维护各个实例,也...
前言: 藉由训练过程中的检查点纪录,可以知道此模型的训练次数,不过若不是特别需要,平常可以注解掉,让...
安装专案 composer global require laravel/installer //将...
今天要解的题目和昨天有关联 是他进阶的2.0 没错,又是爱尔兰人 Irish-Name-Repo 2...
Categorical Encoding Encoding Describe One hot enc...