The article was initially posted on 2018-06-05.
个人浅薄和粗糙的理解,忽略了大量细节,必有疏漏,仅供参考
这里主要是介绍 Promise 的工作机制。数据结构的实现和标准并不一致。
工作机制
const p = new Promise((resolve) => {
// ...
() => {
resolve(result)
}
// ...
})
p.then(function onfulfilled(result) {
// 得到 result
})
核心思想如上所示,用户在需要的时候(同步地/异步地)调用 Promise 提供的 resolve
函数传入 fulfill 的结果 result
,然后内部就会安排调用用户通过 .then
注册的 onfulfilled 回调函数将 result
交回给用户。
then
首先,p.then
一定会返回新的 Promise,得到如下的框架。
Promise.prototype.then = function then(onfulfilled, onrejected) {
const p = this
const np = new Promise((resolve, reject) => {
// TODO
})
return np
}
接着在 TODO 中,定义一个 PromiseReactionJob
,负责 resolve 或 reject 掉返回的新 Promise。
// const np = new Promise((resolve, reject) => {
// ...
/**
* @param {function} reaction - 用户传来的 onfulfilled 或 onrejected
* @param {any} result - fulfill_result 或 reject_reason
*/
function PromiseReactionJob(reaction, result) {
try {
// p.then(() => thenResult, () => thenResult)
// 即 onfulfilled 和 onrejected 调用后的返回值
const thenResult = reaction(result)
// resolve 掉 .then 返回出去的 np
resolve(thenResult)
} catch (e) {
// e 是 reaction(result) 抛出的异常
// reject 掉 np
reject(e)
}
}
// ...
// }
接着是 .then
的核心逻辑:如果 p
的状态是 pending
,则将 PromiseReactionJob
加入 fulfill 和 reject 的回调函数列表暂时保存起来。
否则就将 PromiseReactionJob
直接加入微任务队列。
Promise.prototype.then = function then(onfulfilled, onrejected) {
const op = this
const np = new Promise((resolve, reject) => {
// 1. 看看 reaction 是否 callable,否的话就使用默认的
if (!isCallable(onfulfilled)) onfulfilled = x => x
if (!isCallable(onrejected)) onrejected = (e) => { throw e }
// 2. 定义 PromiseReactionJob
function PromiseReactionJob(reaction, result) { /* ... resolve or reject np ... */ }
// 3. 加入回调函数列表/直接加入微任务队列
if (op._state === 'pending') {
op._fulfill_reactions.push(() => {
// 等到 fulfill 时才将 PromiseReactionJob 加入微任务队列
enqueueJob(() => PromiseReactionJob(onfulfilled, op._fulfill_result))
})
op._reject_reactions.push(() => {
// 等到 reject 时才将 PromiseReactionJob 加入微任务队列
enqueueJob(() => PromiseReactionJob(onrejected, op._reject_reason))
})
} else if (op._state === 'fulfilled') {
// 直接将 PromiseReactionJob 加入微任务队列
enqueueJob(() => PromiseReactionJob(onfulfilled, op._fulfill_result))
} else if (op._state === 'rejected') {
// 直接将 PromiseReactionJob 加入微任务队列
enqueueJob(() => PromiseReactionJob(onrejected, op._reject_reason))
}
})
return np
}
resolve
resolve
函数传给用户,由用户决定什么时候(同步地/异步地)调用。用户调用时传入一个结果 result
。
先给出构造函数的框架。
class Promise {
_state = 'pending'
_fulfill_reactions = []
_reject_reactions = []
constructor(executor) {
// 专门弄个 createResolvingFunctions 主要是为了
// 给每对 reject、resolve 配一个 _already_resolved 变量
const createResolvingFunctions = () => {
let _already_resolved = false
const reject = (reason) => {
// TODO
}
const resolve = (result) => {
// TODO
}
return { resolve, reject }
}
const rf = createResolvingFunctions()
try {
// executor 是用户 `new Promise(executor)` 时传进来的
executor(rf.resolve, rf.reject)
} catch (e) {
rf.reject(e)
}
}
}
resolve
的逻辑是:如果 result
不是 thenable,则直接 fulfill 掉这个 Promise。
否则, result
就是 thenable,则要将 PromiseResolveThenableJob
加入微任务队列。在 PromiseResolveThenableJob
中,才调用 result.then
注册该 thenable 的回调函数
// const createResolvingFunctions = () => {
let _already_resolved = false
const reject = (reason) => {
// 防止重复调用
if (_already_resolved) return
_already_resolved = true
// reject 掉自己
return this._reject(reason)
}
const resolve = (result) => {
// 防止重复调用
if (_already_resolved) return
_already_resolved = true
// 不能 resolve 自己,会造成死循环
if (result === this) return this._reject(new TypeError("selfResolutionError"))
if (isThenable(result)) {
// 将 PromiseResolveThenableJob 加入微任务队列
// resolve 时并不调用 result.then
const PromiseResolveThenableJob = () => {
const rf = createResolvingFunctions()
// 而是在 resolve 之后的微任务 PromiseResolveThenableJob 中才调用 result.then
result.then(rf.resolve, rf.reject)
}
enqueueJob(() => PromiseResolveThenableJob())
return
}
// is not thenable
// 直接用 non-thenable resolve 掉自己
return this._fulfill(result)
}
// return { resolve, reject }
// }
_fulfill、_reject
主要做的事是调用之前注册的回调函数
Promise.prototype._fulfill = function fulfill(result) {
const { _fulfill_reactions } = this
this._fulfill_result = result
// 清空队列,防止内存泄漏
this._fulfill_reactions = this._reject_reactions = undefined
this._state = 'fulfilled'
// 将 PromiseReactionJob 加入微任务队列
_fulfill_reactions.forEach(reaction => reaction())
}
Promise.prototype._reject = function reject(reason) {
const { _reject_reactions } = this
this._reject_reason = reason
// 清空队列,防止内存泄漏
this._fulfill_reactions = this._reject_reactions = undefined
this._state = 'rejected'
// 将 PromiseReactionJob 加入微任务队列
_reject_reactions.forEach(reaction => reaction())
}
Promise.resolve
new Promise(resolve => resolve(x))
和 Promise.resolve(x)
有什么区别呢?
如果 x 的 constructor 不是 Promise,则二者可以看作是等价的
然而,当 x 的 constructor 是 Promise 时,即 Reflect.getPrototypeOf(x)?.constructor === Promise
new Promise(resolve => resolve(x))
返回新的 Promise,而且新的 Promise 要等两轮微任务才会调用它的.then
回调- 而
Promise.resolve(x) === x
Promise.reject
这个就没那么多事了,直接有
Promise.reject(x)
与new Promise((_, reject) => reject(x))
等价Promise.reject(x).catch(reason => x === reason)
catch
Promise.prototype.catch = function catch_(onrejected) {
return this.then(undefined, onrejected)
}
finally
- 会等待
onfinally()
resolve - 无法改变返回值(与
finally
语句不同) - 无法阻止抛出异常
- 可以抛出新异常覆盖原异常
Promise.prototype.finally = function finally_(onfinally) {
const onfulfilled = onfinally
const onrejected = onfinally
if (isCallable(onfinally)) {
onfulfilled = result => Promise.resolve(onfinally()).then(_ => result)
onrejected = reason => Promise.resolve(onfinally()).then((_) => { throw reason })
}
return this.then(onfulfilled, onrejected)
}
推论与应用
推论
const np = p.then(function cb() { return ret })
中,回调函数cb()
返回后,才调用np.resolve(ret)
- 顺序是
fulfill p
->call cb
->fulfill p.then() using cb()'s ret
p.resolve(nonPendingThenable)
时,要间隔 2 轮微任务(在第 3 轮)才调用p.then
注册的回调函数p.resolve(nonThenable)
和p.reject(x)
时不需要隔微任务,下一轮就调用p.then
注册的回调函数了
p.then((x) => { ... }, (y) => {...})
中,x
即p._fulfill_result
不可能是 thenable- 因为
_fulfill_result
的值只在p._fulfill(result)
中设置,而p._fulfill(result)
只在resolve
中的result
不是 thenable 时才会调用 y
就不一样了:它可以是 thenable
- 因为
链式调用
链式调用的实现在 .then
中的 PromiseReactionJob
,通过在原 Promise 的回调函数中调用新 Promise 的 resolve
、reject
,将原 Promise 的状态传给新 Promise。
例如
Promise.resolve()
.then(() => { throw e })
.then(() => 1)
.then(() => 2)
.catch(() => 3)
.then(() => 4)
实际上是
Promise.resolve() // [1]
.then(() => { throw e }) // [2]
.then(() => 1, (e) => { throw e }) // [3]
.then(() => 2, (e) => { throw e }) // [4]
.catch(() => 3) // [5]
.then(() => 4) // [6]
// .then(onfulfilled, onrejected)
// .catch(onrejected)
也就是说,[2] 的 onfulfilled 中主动抛出异常,将 [2] 返回的 Promise reject 掉,调用了注册在那上面的 [3] 的 onrejected,即 (e) => { throw e }
,继续抛异常将 [3] 返回的 Promise reject 掉,调用了注册在那上面的 [4] 的 onrejected,继续抛异常将 [4] 返回的 Promise reject 掉,调用了注册在那上面的 [5] 的 onrejected,正常返回了数字 3 将 [5] 返回的 Promise fulfill 掉,调用了注册在那上面的 [6] 的 onfulfilled,正常返回了数字 4 将 [6] 返回的 Promise fulfill
Node 8 vs Firefox
下面的代码输出什么?来源:Holding on to your Performance Promises (Node Collaborator Summit Berlin May ‘18) - Google Slides
function log(v) {
console.log(`log(${v})`);
return Promise.resolve(undefined);
}
async function countTo(k) {
for (let i = 0; i < k; ++i) await log(i);
}
function tick(v) {
console.log(`Tick ${v}…`);
if (v > 0) Promise.resolve(v - 1).then(tick);
}
tick(10);
countTo(4);
Node 8 输出如下
Tick 10…
log(0)
Tick 9…
log(1)
Tick 8…
log(2)
Tick 7…
log(3)
Tick 6…
Tick 5…
Tick 4…
Tick 3…
Tick 2…
Tick 1…
Tick 0…
Firefox 输出如下
Tick 10…
log(0)
Tick 9…
Tick 8…
Tick 7…
log(1)
Tick 6…
Tick 5…
Tick 4…
log(2)
Tick 3…
Tick 2…
Tick 1…
log(3)
Tick 0…
谁是对的?Firefox 是对的。为什么?
先看下面的代码:
const p0 = Promise.resolve(1)
const p1 = new Promise(resolve => resolve(p0))
const p2 = p1.then(console.log)
根据上面的工作机制可知,要等到第 3 轮微任务才会调用 console.log
:
new
时的resolve
将微任务PromiseResolveThenableJob
加入队列- 第 1 轮微任务
PromiseResolveThenableJob
执行p0.then(rf.resolve)
,由于p0
不是pending
,所以将微任务PromiseReactionJob(rf.resolve, p0._fulfill_result)
加入队列 - 第 2 轮微任务
PromiseReactionJob
执行reaction(p0._fulfill_result)
,即rf.resolve(p0._fulfill_result)
。p0._fulfill_result
是1
不是 thenable,那就变成p1._fulfill(p0._fulfill_result)
,也就是把 p1 用p0._fulfill_result
给 fulfill 了。- p1 fulfill 时,会调用
p1._fulfill_reactions
里保存的回调函数,将微任务PromiseReactionJob(console.log, p1._fulfill_result)
加入微任务队列。 - 此后还会把
reaction(p0._fulfill_result)
的返回值undefined
交给resolve
,但这个resolve
fulfill 的是p0.then()
返回的 Promise。鉴于这个返回的 Promise 没有继续 then 下去,在这里可以忽略它的影响
- p1 fulfill 时,会调用
- 第 3 轮微任务
PromiseReactionJob
执行reaction(p1._fulfill_result)
,即console.log(p1._fulfill_result)
。- 此后还会把
reaction(p1._fulfill_result)
的返回值undefined
交给resolve
,这个resolve
fulfill 的是p1.then()
返回的p2
- 此后还会把
另一方面, async
函数实质上是 Promise
和生成器函数的语法糖(忽略一些异常处理的细节)
await log(i)
=> await Promise.resolve(i)
=> yield Promise.resolve(i)
=> {
// yield Promise.resolve(i) 时,it.next() 返回 Promise.resolve(i)
const result = it.next()
new Promise(resolve => resolve(result)).then(val => it.next(val)) // it.next(val) 标志着 yield 返回
}
=> {
// 令 p0 = result = Promise.resolve(i),标志着开始 await
const p0 = Promise.resolve(i)
// 拆成 p1、p2
const p1 = new Promise(resolve => resolve(p0)) // [1]
// it.next(val) 标志着 await 返回
const p2 = p1.then(val => it.next(val))
}
步骤 [1] 是标准要求的,参见 Await 算法 的第 2、3 步。
所以,从 p0
、p1
、p2
完成定义注册好回调函数,到 it.next(val)
的调用,即从 log(i)
开始被 await
,到 await log(i)
表达式整个返回,应该要间隔 2 轮微任务才对。由此可见,Firefox 先打出 log(0)
,然后间隔 2 轮微任务后再打出 log(1)
,才符合标准。
Node 8 的输出是因为当时的 V8 在 new Promise(resolve => resolve(p0))
时,看到 p0
是 fulfilled 了,就直接把返回的 new Promise(resolve => resolve(p0))
给 fulfilled 了,结果弄巧成拙,不合标准。
不过在 2018 年 7 月的 tc39 会议上,有人指出了 3 轮微任务的问题,认为应该采用 Node 8 的做法,改成 1 轮微任务
参考资料
- ECMAScript® 2019 Language Specification
- Holding on to your Performance Promises (Node Collaborator Summit Berlin May ‘18) - Google Slides