Promise的写法给异步带来了新的变化,当然写法的变化是基于V8执行层的变化 — 微任务队列。ES6中引入了微任务我们先来谈一谈微任务。

微任务

在Event-Driven架构下,异步任务通过事件队列来处理,浏览器环境中,定时器任务、网络请求、交互响应等,都把回调任务加入到事件队列中。只有一个队列,如果一个定时任务1s后执行,但是还有一个定时任务要在500ms后插入10000个div元素,大量的DOM操作就容易阻塞任务的读取执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let start, end;
console.log('Start of script');

// 插入10000个DOM
setTimeout(() => {
start = Date.now()
console.log('Start of heavy DOM operation');
for (let i = 0; i < 10000; i++) {
let element = document.createElement('div');
element.textContent = 'Element ' + i;
document.body.appendChild(element);
}
console.log('End of heavy DOM operation');
}, 0);

setTimeout(() => {
end = Date.now();
console.log('callback to execute 1s later');
console.log(end - start);
}, 1000);

console.log('End of script');
1
2
3
4
5
6
Start of script
End of script
Start of heavy DOM operation
End of heavy DOM operation
callback to execute 1s later
1043

虽然输出的时间是1s左右,但实际上等待的时间取决于DOM操作的时间,远大于1s.

只有一个任务队列,队列中一旦存在long-running任务,比如大量DOM操作,复杂的计算,大量的同步网络请求等,那其后的任务就要等,等到调用栈清空,主线程再次从任务队列取任务。

如果你是等待取出执行的任务,你会怎么想?只有一个队列,我后加入的就要后执行,这太被动了,我能不能自己掌控自己的命运?能,再开个队列。两个队列,运行有先后,那就有了自定义优先级的权利。想先运行,就加到先执行的队列 — 微任务队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let start, end;
console.log('Start of script');
start = Date.now()
// 插入10000个DOM
setTimeout(() => {
console.log('Start of heavy DOM operation');
for (let i = 0; i < 10000; i++) {
let element = document.createElement('div');
element.textContent = 'Element ' + i;
document.body.appendChild(element);
}
console.log('End of heavy DOM operation');
}, 0);

queueMicrotask(() => {
end = Date.now();
console.log('callback to execute 1s later');
console.log(end - start);
}, 1000);

console.log('End of script');
1
2
3
4
5
6
Start of script
End of script
callback to execute 1s later
1
Start of heavy DOM operation
End of heavy DOM operation

任务放到微任务队列之后,虽然在setTimeout之后入的队列,因为队列不同,优先级更高,所以先执行,不会被大量DOM操作任务阻塞。

所以,有了微任务队列,赋予了定义异步任务执行优先级的能力,异步代码更可控。

从图示可以看到,调用栈清空之后,优先执行微任务队列中的任务。这也是微任务的执行时间 — 主程序执行完之后,宏任务队列开始之前。

关于[什么时候使用微任务](在 JavaScript 中通过 queueMicrotask() 使用微任务 - Web API 接口参考 | MDN),官方文档也给出了例子。

接下来看看更常用的Promise.

什么是Promise

微任务和Promise什么关系?

1
2
3
4
5
6
// ...
let obj = {
then: () => console.log('callback to execute 1s later')
};
Promise.resolve(obj);
// ...

把上面queueMicrotask中的代码替换为一个包含then方法的对象,用Promise resolve后,发现也能达到同样的效果,也就是说,.then方法中的回调也会加入到微任务队列。

Promise在JavaScript中最初的定义就是[Thenable](Promise - JavaScript | MDN).但是后来引入了Promises/A+规范,只使用.then方法不能实现微任务。

从上图看,Promise可以理解为一个状态机,初始状态是pending,经过同步执行resolve或者reject之后,相应的状态变为fufilled和rejected.状态变更后的响应回调就会加入到微任务队列,在全部同步代码执行完毕后,再执行。

举个例子,你去买彩票,双色球,买完老板打了票给你,说晚上开奖,你揣着这张票,等啊等。晚上开奖,无非两种结果:中了、没中。中了,第二天去兑奖;没中,就扔掉。

在这里,买彩票的操作可以看作一个Promise,没开奖时,是未知状态(pending),结果只能是成功(fulfilled)、失败(rejected)两种状态中的一种。成功了,就执行兑奖这个回调;失败了,就执行扔掉这个回调。

如何使用Promise

Promise是一把利器,但是如何使用也是个问题。排除了错误的用法,就能知道如何正确的使用。来看一些错误的用例:

  1. then未返回
1
2
3
4
5
6
7
addCart()
.then((items) => {
createOrder(items)
})
.then((order) => processPayment(order))
.then(() => console.log('payment success'))
.catch((err) => console.log(err))

在上面的例子中,createOrder执行后,结果并没有返回给下一个then.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Promise<T> {
then<TResult1 = T, TResult2 = never>(
onfulfilled?:
| ((value: T) => TResult1 | PromiseLike<TResult1>)
| undefined
| null,
onrejected?:
| ((reason: any) => TResult2 | PromiseLike<TResult2>)
| undefined
| null
): Promise<TResult1 | TResult2>;
catch<TResult = never>(
onrejected?:
| ((reason: any) => TResult | PromiseLike<TResult>)
| undefined
| null
): Promise<T | TResult>;
}

可以看到then是要返回Promise的,所以支付并不会成功。

  1. 混用Promise和callback
1
2
3
4
5
6
7
8
9
10
function (a) {
const b = doSyncStuff(a);

return promiseReturningFn(b)
.then(result => {
cont k = doWhatEver(result);
return doSyncOrAsyncStuff(result);
});
// no catch() because the promise is returned and caller needs to have a catch()
}

这种写法下,如果报错,同步代码会抛出错误,但是promise代码并没有错误处理,这样的错误处理方式并不统一

  1. 嵌套Promise

甚至还有一些糟糕的认为Promise就是回调

  1. 在then中做错误处理
1
2
3
4
5
6
promiseFunc()
.then(value => {
// code respond to resolve
}, err => {
// code respond to reject
})

catch(cb)可以看作then(null, cb),如果onFulfilled报错了,异常并没有捕获,所以还是要统一catch()处理。

如果使用Promise,从参数、返回值到错误处理,都完全是自成一套规范,如果按原来的回调式处理,最好还是用util/promisify处理后,统一用Promise处理。

结语

本文先介绍了微任务队列,它赋予了我们自定义异步任务优先级的能力,而Promise.then可以创建微任务。要使用Promise,就要遵循Promise/A+规范,才能更好地处理异步操作。

参考文档

Promise - JavaScript | MDN

25. Promises for asynchronous programming

https://stackoverflow.com/questions/62037166/can-i-mix-callbacks-and-async-await-patterns-in-nodejs

https://groups.google.com/g/exploring-es6/c/vZDdN8dCx0w

Broken Promises - James Snell, NearForm - YouTube