Promise
是一种编程模式,用于处理异步操作的结果或者在将来某个时间点产生的值。它可以帮助开发者更优雅地处理异步编程,使代码更易于阅读、维护和理解。除了 JavaScript,许多其他编程语言也引入了类似 Promise 的概念来处理异步操作,例如:
- Python: 使用
async
和await
关键字来实现类似 Promise 的异步编程模式。 - Java: 引入了
CompletableFuture
类来处理异步操作和组合多个异步操作。 - C#: 引入了
Task
和async/await
关键字来处理 异步编程。 - Swift: 使用
async
和await
关键字来实现异步操作。 - Rust: 提供了
std::future
和async/await
来支持异步编程。
关于 Promise
的标准,最初是由 JavaScript
社区提出的,后来在 ECMAScript 6(ES6)
规范中正式纳入了 Promise 对象。其实Promise
规范有很多,如Promise/A
,Promise/B
,Promise/D
以及 Promise/A
的升级版 Promise/A+
。ES6
中采用了 Promise/A+
规范。
(注:Promise/A+规范不包含race
、all
、catch
等方法的说明,这些实现是ES6
自定义的规范)
在 JavaScript
中,Promise
是一种内置的对象类型,用于处理异步操作。一个 Promise
可以处于以下三种状态之一:
Pending
(进行中):初始状态,异步操作尚未完成也没有被拒绝。Fulfilled
(已完成):异步操作成功完成,并返回一个值。Rejected
(已拒绝):异步操作失败,并返回一个错误。
在解析 Promise
内部原理之前,先来看几个有关代码运行顺序的问题:
Promise 代码执行顺序
- PS:第一问
js
new Promise((res,rej)=>{
console.log(111);
res(222)
}).then(res=>{
console.log(res);
return 444
}).then(res=>{
console.log(res)
return 555
}).then(res=>{
console.log(res);
return 555
}).then(res=>{
console.log(res)
});
Promise.resolve(333).then(res=>{
console.log(res)
return 'aaa'
}).then(res=>{
console.log(res)
return 'bbb'
}).then(res=>{
console.log(res)
return 'ccc'
}).then(res=>{
console.log(res)
return 'ddd'
})
输出顺序为:111 222 333 444 aaa 555 bbb 555 ccc
是不是太简单了,继续:
- PS:第二问
js
new Promise(resolve => {
resolve(
new Promise(resolve => {
resolve(1);
})
);
}).then(res => {
console.log('1');
});
new Promise(resolve => {
resolve(2);
})
.then(() => {
console.log('2');
})
.then(() => {
console.log('3');
})
.then(() => {
console.log('4');
});
是不是想说 output: 1 2 3 4
实则输出顺序为:1 3 2 4
是不是有点迷糊了?不着急,后面会说,继续看下一个:
- PS:第三问
js
Promise.resolve().then(() => {
console.log(0);
return Promise.resolve(4);
}).then((res) => {
console.log(res)
})
Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() =>{
console.log(6);
})
是不是也不太清楚? 别着急,后面会讲解
输出顺序为:0 1 2 3 4 5 6
根据以上三问,我们接下来逐一讲解:
第一问:
自然不必说,只是需要注意几点:
Promise
状态一旦改变就永远不会变化- 推送进入微任务队列的不是
resove(value)
或reject(error)
而是以微任务的队列执行的then
中的回调函数,该回调函数是在resove(value)
或reject(error)
执行获取到"结果"之后立即进入的
所以第一问中是微任务交替进行输出
第二问:
和第一问不一样的地方是开始的 Promise
中 resolve
的参数值是一个 Promise
类型的数据,也就是 new Promise(resolve => { resolve(1); })
这段代码,如何执行这段代码,这里我们必须要说的就是 ECMA 规范,
注意:你可能看过 c++
编写的的V8
引擎中关于 Promise
的源码,有兴趣的小伙伴可以自行了解,这里我们不做过多探讨。而是从 ECMA 规范角度来看,为什么输出是这样,以及为何这样设计。
首先是 ECMA
规范中把 Promise
的微任务分成了两种类型,NewPromiseReactionJob
和 NewPromiseResolveThenableJob
;
NewPromiseReactionJob
Promise
确定状态时执行 then()
中注册的回调,会产生这种微任务。也就是:当前 Promise
状态已经改变,接着调用了 then()
,例如:
js
Promise.resolve(999).then(res => console.log(res));
res => console.log(res)
该回调函数就处在这种微任务之中。规范中对于 Promise.prototype.then
的描述如下:
假设状态改变为 fulfilled
,继续查看 PerformPromiseThen
中对 fulfilled
状态的处理:
由此可见这里创建了一个 NewPromiseReactionJob
微任务,并将其加入到了微任务队列中,继续查看NewPromiseReactionJob 内部是如何执行的:
这里该微任务主要做如下处理:
NewPromiseReactionJob
接受参数reaction
(一个PromiseReaction
记录)和参数(一个ECMAScript
语言值),并返回一个带有字段[[Job]]
(一个闭包)和[[Realm]]
(记录值或null)的记录 。 它返回一个新的闭包,将适当的处理程序应用于传入值,并使用处理程序的返回值来完成或拒绝与该处理程序关联的派生Promise
; 上述过程简要总结为:- 执行
handler
闭包,handler
就是then()
中注册的回调函数,得到其返回结果。 - 对
then()
中产生的新Promise
执行resolve
(返回结果) 或reject
(返回结果)。
- 执行
NewPromiseResolveThenableJob
这个微任务就是本文的重点,上述的 NewPromiseReactionJob
微任务是平时 Promise
执行的基本操作,首先看一下规范介绍:
这里是 resolve
函数执行的规范:
这里表明,如果一个 对象数据的 then 属性是可以调用执行(也即是它是一个 Function ),那么此时这个对象就是被称作thenable
对象。调用 resolve()
传递的参数值如果是一个 thenable
对象,就会产生 NewPromiseResolveThenableJob
这种微任务了。 举个例子说明一下:
- 先写一段 自定义的
Promise
代码,这里使用queueMicrotask
api
模拟微任务
js
const _my_promise = function(executor){
let value = null;
const res = (v) =>{
value = v;
}
const rej = (e) =>{
value = e;
}
this.then= function (onFulfilled, onRejected) {
queueMicrotask(() => {
console.log(value,"pre_load")
})
}
try {
executor(res, rej);
} catch (err) {
rej(err);
}
}
- 举例说明例子二:
js
new Promise(resolve => {
resolve(
new _my_promise(resolve => {
resolve(4);
})
);
}).then(res => {
console.log(res);
});
Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() =>{
console.log(6);
})
看一下输出: 接下来看看这个微任务的内容:1 4 2 'pre_load' 3 5 6
- 举例说明例子三:
js
Promise.resolve().then(() => {
console.log(0);
return new _my_promise((res,rej)=>{
res(4)
});
}).then((res) => {
console.log(res)
})
Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() =>{
console.log(6);
})
看一下输出: 接下来看看这个微任务的内容:0 1 2 4 'pre_load' 3 5 6
很明显。这里我并没有执行 then
函数,为什么例二和例三依然输出 console.log(value,"pre_load")
这句话呢?没错,就是因为自定义 _my_promise
对象是一个 thenable
对象,因为具有回调函数 then
,接下来看一下 这个微任务执行的内容
也即是在此产生了如同下式的代码,也就是执行了 then
的回调函数
注:resolve 中参数值为 Promise 和 then 的回调函数中 然绘制为 Promise 对象实质上是一样的,走的微任务路线相似,所以例二和例三可以放在一起理解
js
thenable.then(resolve, reject);
这意味着什么呢?也就是说明以下两段代码等价:
js
其一:
Promise.resolve().then(() => {
console.log(0);
return new Promise((res,rej)=>{
res(4)
});
}).then((res) => {
console.log(res)
})
Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() =>{
console.log(6);
})
其二:
Promise.resolve().then(() => {
console.log(0);
return new Promise((res,rej)=>{
res(4)
}).then(res => res); // 多加一个 then 不影响最后执行顺序,但是如果此时再加一个then,就会再多出一个微任务,意义不相同
}).then((res) => {
console.log(res)
})
Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() =>{
console.log(6);
})
但是为什么和原生 Promise
也即是例子三的输出结果不一致呢? 这就是因为,如果 thenable
对象是一个 Promise
对象,那么这个微任务执行之后又会产生一个新的微任务,既是例三中,return new Promise((res,rej)=>{ res(4) });
这里产生了两次微任务,那么为什么要这样,是设计的缺陷还是有何目的呢?看一下规范是如何说的:
直译就是:
此Job使用提供的 thenable 及其 then 方法来解决给定的 Promise。 此过程必须作为Job进行,以确保 then 方法的状态转换发生在任何周围代码的状态转换完成之后。
注意:这里应该是想说要等周围的同步代码执行完后才会执行这个,因为 thenable
对象的不一定是 Promise
实例,也可能是用户创建的任何对象;如果这个对象的 then
是同步方法,那么这样做就可以保证 then
的执行顺序也是在微任务中。
PS:走马观花式的看一小部分所谓的"V8底层源码",并不意味着你能了解其设计理念和思想,不要遇到问题就拿源码说事。因为很多人并没有精力和耐心去了解这些东西,甚至是 ECMA
规范也不见得别人想了解。当然,有时了解源码固然有必要,但是提问的人真的是想听你述说底层源码的事情吗?或者该阶段的开发者目前所涉及的领域对于这个问题需要理解到这种程度吗?不一定,大部分开发者也许只想要解决实际问题,或得到一个相对满意的答复,仅此而已。以上只是个人见解。