共计 7570 个字符,预计需要花费 19 分钟才能阅读完成。
更好的阅度体验
前言
API
Promise 特点
状态跟随
V8 中的 async await 和 Promise
实现一个 Promise
参考
前言
作为一个前端开发,使用了 Promise 一年多了,一直以来都停留在 API 的调用阶段,没有很好的去深入。刚好最近阅读了 V8 团队的一篇如何实现更快的 async await,借着这个机会整理了 Promise 的相关理解。文中如有错误,请轻喷~
API
Promise 是社区中对于异步的一种解决方案,相对于回调函数和事件机制更直观和容易理解。ES6 将其写进了语言标准,统一了用法,提供了原生的 Promise 对象。
这里只对 API 的一些特点做记录,如果需要详细教程,推荐阮老师的 Promise 对象一文
new Promise – 创建一个 promise 实例
Promise.prototype.then(resolve, reject) –then 方法返回一个新的 Promise 实例
Promise.prototype.catch(error) –.then(null, rejection) 或.then(undefined, rejection) 的别名,用于指定发生错误时的回调函数。– 错误会一直传递,直到被 catch, 如果没有 catch,则没有任何反应 –catch 返回一个新的 Promise 实例
Promise.prototype.finally() – 指定不管 Promise 对象最后状态如何,都会执行的操作。– 实现如下:
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason})
);
};
Promise.all([promise Array]) – 将多个 Promise 实例,包装成一个新的 Promise 实例 – 所有子 promise 执行完成后,才执行 all 的 resolve,参数为所有子 promise 返回的数组 – 某个子 promise 出错时,执行 all 的 reject,参数为第一个被 reject 的实例的返回值 – 某个子 promise 自己 catch 时,不会传递 reject 给 all,因为 catch 重新返回一个 promise 实例
Promise.race([promise Array]) – 将多个 Promise 实例,包装成一个新的 Promise 实例。– 子 promise 有一个实例率先改变状态,race 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 race 的回调函数。
Promise.resolve() – 将现有对象转为 Promise 对象 – 参数是 promise 实例,原封不动的返回 – 参数是一个 thenable 对象 将这个对象转为 Promise 对象,状态为 resolved – 参数是一个原始值 返回一个新的 Promise 对象,状态为 resolved – 不带有任何参数 返回一个 resolved 状态的 Promise 对象。– 等价于如下代码
Promise.resolve(‘foo’)
// 等价于
new Promise(resolve => resolve(‘foo’))
Promise.reject() – 返回一个新的 Promise 实例,该实例的状态为 rejected –Promise.reject() 方法的参数,会原封不动地作为 reject 的理由,变成后续方法的参数。
Promise 特点
很多文章都是把 resolve 当成 fulfilled,本文也是,但本文还有另外一个 resolved,指的是该 Promise 已经被处理,注意两者的区别
1. 对象具有三个状态,分别是 pending(进行中)、fulfilled(resolve)(已成功)、reject(已失败),并且对象的状态不受外界改变,只能从 pending 到 fulfilled 或者 pending 到 reject。
2. 一旦状态被改变,就不会再变,任何时候都能得到这个结果,与事件回调不同,事件回调在事件过去后无法再调用函数。
3. 一个 promise 一旦 resolved,再次 resolve/reject 将失效。即只能 resolved 一次。
4. 值穿透,传给 then 或者 catch 的参数为非函数时,会发生穿透(下面有示例代码)
5. 无法取消,Promise 一旦运行,无法取消。
6. 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部
7. 处于 pending 时,无法感知 promise 的状态(刚刚开始还是即将完成)。
值穿透代码:
new Promise(resolve=>resolve(8))
.then()
.then()
.then(function foo(value) {
console.log(value) // 8
})
状态追随
状态追随的概念和下面的 v8 处理 asyac await 相关联
状态跟随就是指将一个 promise(代指 A)当成另外一个 promise(代指 B)的 resolve 参数,即 B 的状态会追随 A。如下代码所示:
const promiseA = new Promise((resolve) => {
setTimeout(() => {
resolve(‘ccc’)
}, 3000)
})
const promiseB = new Promise(res => {
res(promiseA)
})
promiseB.then((arg) => {
console.log(arg) // print ‘ccc’ after 3000ms
})
按理说,promiseB 应该是已经处于 resolve 的状态,但是依然要 3000ms 后才打印出我们想要的值,这难免让人困惑
在 ES 的标准文档中有这么一句话可以帮助我们理解:
A resolved promise may be pending, fulfilled or rejected.
就是说一个已经处理的 promise,他的状态有可能是 pending, fulfilled 或者 rejected。这与我们前面学的不一样啊,resolved 了的 promise 不应该是处于结果状态吗?这确实有点反直觉,结合上面的例子看,当处于状态跟随时,即使 promiseB 立即被 resolved 了,但是因为他追随了 promiseA 的状态,而 A 的状态则是 pending,所以才说处于 resolved 的 promiseB 的状态是 pending。
再看另外一个例子:
const promiseA = new Promise((resolve) => {
setTimeout(() => {
resolve(‘ccc’)
}, 3000)
})
const promiseB = new Promise(res => {
setTimeout(() => {
res(promiseA)
}, 5000)
})
promiseB.then((arg) => {
console.log(arg) // print ‘ccc’ after 5000ms
})
其实理解了上面的话,这一段的代码也比较容易理解,只是因为自己之前进了牛角尖,所以特意记录下来:
3s 后 promiseA 状态变成 resolve
5s 后 promiseB 被 resolved,追随 promiseA 的状态
因为 promiseA 的状态为 resolve,所以打印 ccc
V8 中的 async await 和 Promise
在进入正题之前,我们可以先看下面这段代码:
const p = Promise.resolve();
(async () => {
await p;
console.log(“after:await”);
})();
p.then(() => {
console.log(“tick:a”);
}).then(() => {
console.log(“tick:b”);
});
V8 团队的博客中,说到这段代码的运行结果有两种:
Node8(错误的):
tick a
tick b
after: await
Node10(正确的):
after await
tick a
tick b
ok,问题来了,为啥是这个样子?先从 V8 对于 await 的处理说起,这里引用一张官方博客的图来说明 Node8 await 的伪代码:
Node8 await
对于上面的例子代码翻译过来就(该代码引用自 V8 是怎么实现更快的 async await)是:
const p = Promise.resolve();
(() => {
const implicit_promise = new Promise(resolve => {
const promise = new Promise(res => res(p));
promise.then(() => {
console.log(“after:await”);
resolve();
});
});
return implicit_promise;
})();
p.then(() => {
console.log(“tick:a”);
}).then(() => {
console.log(“tick:b”);
});
很明显,内部那一句 new Promise(res => res(p)); 代码就是一个状态跟随,即 promise 追随 p 的状态,那这跟上面的结果又有什么关系?
在继续深入之前,我们还需要了解一些概念:
task 和 microtask
JavaScript 中有 task 和 microtask 的概念。Task 处理 I/O 和计时器等事件,一次执行一个。Microtask 为 async/await 和 promise 实现延迟执行,并在每个任务结束时执行。总是等到 microtasks 队列被清空,事件循环执行才会返回。
如官方提供的一张图:
EnqueueJob、PromiseResolveThenableJob 和 PromiseReactionJob
EnquequeJob:存放两种类型的任务,即 PromiseResolveThenableJob 和 PromiseReactionJob,并且都是属于 microtask 类型的任务
PromiseReactionJob:可以通俗的理解为 promise 中的回调函数
PromiseResolveThenableJob(promiseToResolve, thenable, then):创建和 promiseToResolve 关联的 resolve function 和 reject function。以 then 为调用函数,thenable 为 this,resolve function 和 reject function 为参数调用返回。(下面利用代码讲解)
状态跟随的内部
再以之前的代码为例子
const promiseA = new Promise((resolve) => {
resolve(‘ccc’)
})
const promiseB = new Promise(res => {
res(promiseA)
})
当 promiseB 被 resolved 的时候,也就是将一个 promise(代指 A)当成另外一个 promise(代指 B)的 resolve 参数,会向 EnquequeJob 插入一个 PromiseResolveThenableJob 任务,PromiseResolveThenableJob 大概做了如下的事情:
() => {
promiseA.then(
resolvePromiseB,
rejectPromiseB
);
}
并且当 resolvePromiseB 执行后,promiseB 的状态才变成 resolve,也就是 B 追随 A 的状态
Node 8 中的流程
1. p 处于 resolve 状态,promise 调用 then 被 resolved,同时向 microtask 插入任务 PromiseResolveThenableJob
2. p.then 被调用,向 microtask 插入任务 tickA
3. 执行 PromiseResolveThenableJob, 向 microtask 插入任务 resolvePromise(如上面的 promiseA.then(…))
4. 执行 tickA(即输出 tick: a),返回一个 promise,向 microtask 插入任务 tickB
5. 因为 microtask 的任务还没执行完,继续
6. 执行 resolvePromise,此时 promise 终于变成 resolve,向 microtask 插入任务 ’after await’
7. 执行 tickB(即输出 tick: b)
8. 执行 ’after await’(即输出 ’after await’)
Node 10 await
老规矩,先补一张伪代码图:翻译过来就是酱紫:
const p = Promise.resolve();
(() => {
const implicit_promise = new Promise(resolve => {
const promise = Promise.resolve(p)
promise.then(() => {
console.log(“after:await”);
resolve();
});
});
return implicit_promise;
})();
p.then(() => {
console.log(“tick:a”);
}).then(() => {
console.log(“tick:b”);
});
因为 p 是一个 promise,然后 Promise.resolve 会直接将 P 返回,也就是
p === promise // true
因为直接返回了 p,所以省去了中间两个 microtask 任务,并且输出的顺序也变得正常,也就是 V8 所说的更快的 async await
实现一个 Promise
先实现一个基础的函数
function Promise(cb) {
const that = this
that.value = undefined // Promise 的值
that.status = ‘pending’ // Promise 的状态
that.resolveArray = [] // resolve 函数集合
that.rejectArray = [] // reject 函数集合
function resolve(value) {
if (value instanceof Promise) {
return value.then(resolve, reject)
}
setTimeout(function() {
if (that.status === ‘pending’) {// 处于 pending 状态 循环调用
that.value = value
that.status = ‘resolve’
for(let i = 0; i < that.resolveArray.length; i++) {
that.resolveArray[i](value)
}
}
})
}
function reject(reason) {
if (reason instanceof Promise) {
return reason.then(resolve, reject)
}
setTimeout(function() {
if (that.status === ‘pending’) {// 处于 pending 状态 循环调用
that.value = reason
that.status = ‘reject’
for(let i = 0; i < that.rejectArray.length; i++) {
that.rejectArray[i](reason)
}
}
})
}
try {
cb(resolve, reject)
} catch (e) {
reject(e)
}
}
Promise.prototype.then = function(onResolve, onReject) {
var that = this
var promise2 // 返回的 Promise
onResolve = typeof onResolve === ‘function’ ? onResolve : function(v) {return v} // 如果不是函数 则处理穿透值
onReject = typeof onReject === ‘function’ ? onReject : function(v) {return v} // 如果不是函数 则处理穿透值
if (that.status === ‘resolve’) {
return promise2 = new Promise(function(resolve, reject) {
setTimeout(function() {
try {
const x = onResolve(that.value)
if (x instanceof Promise) {// 如果 onResolved 的返回值是一个 Promise 对象,直接取它的结果做为 promise2 的结果
x.then(resolve, reject)
} else {
resolve(x)
}
} catch (e) {
reject(e)
}
})
})
}
if (that.status === ‘reject’) {
return promise2 = new Promise(function(resolve, reject) {
setTimeout(function() {
try {
const x = onResolve(that.value)
if (x instanceof Promise) {// 如果 onResolved 的返回值是一个 Promise 对象,直接取它的结果做为 promise2 的结果
x.then(resolve, reject)
} else {
reject(x)
}
} catch (e) {
reject(e)
}
})
})
}
if (that.status === ‘pending’) {
return promise2 = new Promise(function(resolve, reject) {
that.resolveArray.push(function(value) {
try {
var x = onResolve(value)
if (x instanceof Promise) {
x.then(resolve, reject)
}
} catch (e) {
reject(e)
}
})
that.rejectArray.push(function(reason) {
try {
var x = onReject(reason)
if (x instanceof Promise) {
x.then(resolve, reject)
}
} catch (e) {
reject(e)
}
})
})
}
}
Promise.prototype.catch = function(onReject) {
return this.then(null, onReject)
}
参考
v8 是怎么实现更快的 await?深入理解 await 的运行机制 V8 中更快的异步函数和 promise 剖析 Promise 内部结构,一步一步实现一个完整的、能通过所有 Test case 的 Promise 类 PromiseA+ ES6 入门 深入 Promise