从实践到原理
Promise/A+ 规范,咱们晚点在看
很多人会告诉你,写 Promise 的第一步,就是认真阅读Promise/A+规范(Promise/A+ 规范的原文大家可以点击 promisesaplus.com/ 查看)。
为什么说要先读规范?因为规范就意味着标准,它是对你目标产物的特性的约束——必须得符合我这里说给你的这些特征,你才能是算是个 Promise。我们可以认为,开发 Promise 的这个过程,也像是在写一个需求,而 Promise/A+ 规范,就是我们的需求文档。
然而对大多数同学来说,在学习这个阶段,你需要的本来就不是一份需求文档,而是一份学习指南。多数同学只要点开这份“需求文档”扫上一眼,他很可能就已经不想写 Promise 了——规范,本质上就是把对新手来说晦涩的知识,以生硬的形式表达了出来。对高手来说,照着规范做实现,不是啥难事。但是对学习者来说,这无疑于在没学完课本、没做过练习题的情况下就被要求去参加考试了,非常容易产生挫败感进而放弃。
其实这个顺序倒过来比较合理——先跟着我撸一个 Promise 出来,在写的过程中,我会一点一点告诉你,为什么要这样做,规范对此是如何描述的。在这个过程中,你对 Promise/A+ 规范的认知和理解会从无到有,从模糊到通透。写完之后,再自己回头去阅读规范原文,你便会发现那些原本看似晦涩的条条框框,一下子变得生动鲜活起来了。此时再去细细琢磨里面的每一句话,就会越读越有味道。“从实践到原理”的用意就在此。
快速上手:executor 与三种状态
我们现在回忆一下之前咱们用过的 Promise。从使用的感受上来说,一个 Promise 应该具备的最基本的特征,至少有以下两点:
- 可以接收一个 executor 作为入参
- 具备 pending、fulfilled 和 rejected 这三种状态
我们先从这最基本的轮廓入手(解析在逐行注释里,本节注释非常重要)
function MyPromise(executor) {
// value 记录异步任务成功的执行结果
this.value = null;
// reason 记录异步任务失败的原因
this.reason = null;
// status 记录当前状态,初始化是 pending
this.status = 'pending';
// 把 this 存下来,后面会用到
var self = this;
// 定义 resolve 函数
function resolve(value) {
// 异步任务成功,把结果赋值给 value
self.value = value;
// 当前状态切换为 fulfilled
self.status = 'fulfilled';
}
// 定义 reject 函数
function reject(reason) {
// 异步任务失败,把结果赋值给 value
self.reason = reason;
// 当前状态切换为 rejected
self.status = 'rejected';
}
// 把 resolve 和 reject 能力赋予执行器
executor(resolve, reject);
}
then 方法的行为
每一个 promise 实例一定有个 then 方法,由此我们不难想到,then 方法应该装在 Promise 构造函数的原型对象上(解析在逐行注释里,本节注释非常重要)
// then 方法接收两个函数作为入参(可选)
MyPromise.prototype.then = function(onResolved, onRejected) {
// 注意,onResolved 和 onRejected必须是函数;如果不是,我们此处用一个透传来兜底
if (typeof onResolved !== 'function') {
onResolved = function(x) {return x};
}
if (typeof onRejected !== 'function') {
onRejected = function(e) {throw e};
}
// 依然是保存 this
var self = this;
// 判断是否是 fulfilled 状态
if (self.status === 'fulfilled') {
// 如果是 执行对应的处理方法
onResolved(self.value);
} else if (self.status === 'rejected') {
// 若是 rejected 状态,则执行 rejected 对应方法
onRejected(self.reason);
}
};
运行下
把咱们的 MyPromise 丢进控制台跑跑看吧
new MyPromise(function(resolve, reject){
resolve('成了!');
}).then(function(value){
console.log(value);
}, function(reason){
console.log(reason);
});
// 输出 “成了!”
new MyPromise(function(resolve, reject){
reject('错了!');
}).then(function(value){
console.log(value);
}, function(reason){
console.log(reason);
});
// 输出“错了!”
OK!各位如果没敲错字的话,咱们手写版的 MyPromise 已经妥妥地跑起来了哈。现在骨骼有了,我们给它加点血肉、再画上眉毛眼睛,就是一个人模人样的 Promise 了
链式调用
想必大家还记得,在 Promise 中,then 方法和 catch 方法都是可以通过链式调用这种形式无限调用下去的。这里先给大家透个底儿:Promise/A+ 规范里,其实压根儿没提 catch 的事儿,它只强调了 then 的存在、约束了 then 的行为。所以咱们此处,就是要实现 then 的链式调用。
要想实现链式调用,咱们考虑以下几个重大的改造点:
- then方法中应该直接把 this 给 return 出去(链式调用常规操作);
- 链式调用允许我们多次调用 then,多个 then 中传入的 onResolved(也叫onFulFilled) 和 onRejected 任务,我们需要把它们维护在一个队列里;
- 要想办法确保 then 方法执行的时机,务必在 onResolved 队列 和 onRejected 队列批量执行前。不然队列任务批量执行的时候,任务本身都还没收集完,就乌龙了。一个比较容易想到的办法就是把批量执行这个动作包装成异步任务,这样就能确保它一定可以在同步代码之后执行了。
OK,明确了改造点之后,咱们动手来完善构造函数的代码
function MyPromise(executor) {
// value 记录异步任务成功的执行结果
this.value = null;
// reason 记录异步任务失败的原因
this.reason = null;
// status 记录当前状态,初始化是 pending
this.status = 'pending';
// 缓存两个队列,维护 fulfilled 和 rejected 各自对应的处理函数
this.onResolvedQueue = [];
this.onRejectedQueue = [];
// 把 this 存下来,后面会用到
var self = this;
// 定义 resolve 函数
function resolve(value) {
// 如果不是 pending 状态,直接返回
if (self.status !== 'pending') {
return;
}
// 异步任务成功,把结果赋值给 value
self.value = value;
// 当前状态切换为 fulfilled
self.status = 'fulfilled';
// 用 setTimeout 延迟队列任务的执行
setTimeout(function(){
// 批量执行 fulfilled 队列里的任务
self.onResolvedQueue.forEach(resolved => resolved(self.value));
});
}
// 定义 reject 函数
function reject(reason) {
// 如果不是 pending 状态,直接返回
if (self.status !== 'pending') {
return;
}
// 异步任务失败,把结果赋值给 value
self.reason = reason;
// 当前状态切换为 rejected
self.status = 'rejected';
// 用 setTimeout 延迟队列任务的执行
setTimeout(function(){
// 批量执行 rejected 队列里的任务
self.onRejectedQueue.forEach(rejected => rejected(self.reason));
});
}
// 把 resolve 和 reject 能力赋予执行器
executor(resolve, reject);
}
相应地,then 方法也需要进行改造。除了返回 this 以外,现在我们会把 fulfilled 和 rejected 任务没有完全被推入队列时的情况,全部视为 pending 状态。于是在 then 方法中,我们还需要对 pending 做额外处理
// then 方法接收两个函数作为入参(可选)
MyPromise.prototype.then = function(onResolved, onRejected) {
// 注意,onResolved 和 onRejected必须是函数;如果不是,我们此处用一个透传来兜底
if (typeof onResolved !== 'function') {
onResolved = function(x) {return x};
}
if (typeof onRejected !== 'function') {
onRejected = function(e) {throw e};
}
// 依然是保存 this
var self = this;
// 判断是否是 fulfilled 状态
if (self.status === 'fulfilled') {
// 如果是 执行对应的处理方法
onResolved(self.value);
} else if (self.status === 'rejected') {
// 若是 rejected 状态,则执行 rejected 对应方法
onRejected(self.reason);
} else if (self.status === 'pending') {
// 若是 pending 状态,则只对任务做入队处理
self.onResolvedQueue.push(onResolved);
self.onRejectedQueue.push(onRejected);
}
return this
};
再运行下
现在我们来验证链式调用是否能生效
const myPromise = new MyPromise(function (resolve, reject) {
resolve('成了!');
});
myPromise.then((value) => {
console.log(value)
console.log('我是第 1 个任务')
}).then(value => {
console.log('我是第 2 个任务')
});
// 依次输出 “成了!” “我是第 1 个任务” “我是第 2 个任务”
输出结果如下:
可以看出,我们的链式调用生效了!
不过,想必细心的同学早已看出,我们现在实现的这个版本的链式调用,相比真实 Promise 的链式调用来说,还是非常单薄的。那么它到底单薄在哪?要想实现一个更完整的链式调用,咱还需要理解Promise 决议程序,好了我们继续咯
决议程序
现有链式调用缺陷分析
我们刚写出来这个 MyPromise,最明显的一个缺陷就是下一个 then 拿不到上一个 then 的结果
const myPromise = new MyPromise(function (resolve, reject) {
resolve('成了!');
});
myPromise.then((value) => {
console.log(value)
console.log('我是第 1 个任务')
return '第 1 个任务的结果'
}).then(value => {
// 此处 value 期望输出 '第 1 个任务的结果'
console.log('第二个任务尝试拿到第 1 个任务的结果是:', value)
});
这段代码里我们尝试在第 2 个 then 中拿到第 1 个 then 中的结果,然而实际的输出却是:
第二个 then 好像无视了第一个 then 的结果,仍然获取到的是我们在 Promise 执行器中 resolve 出的那个最初的值——这显然是不合理的。
事实上,除了这个最明显的缺陷,我们现在实现出来这个 Promise 还有很多能力上的问题,比如说 thenable 对象的特殊处理缺失、比如异常处理缺失等等,这些问题可以用一句话来归纳 —— 对 then 方法的处理过于粗糙。
重新审视 then 方法——理解 Promise 决议程序
前面我们说过,整个 Promise 规范,在方法层面,基本就是围绕着 then 打转。 其中一个最需要引起大家注意的东西叫做 Promise Resolution Procedure(Promise决议程序)。这个名字翻译过来很绕,尤其是“决议”这个动作,看上去挺唬人的。其实这里的“决议”,描述的就是 resolve 这个动作。决议程序,约束的就是 resolve 应该如何表现。这个动作和 then 息息相关,所以要想把 then 方法完善起来,我们必须对决议程序的内容有细致的了解。我们一起来看看 Promise/A+ 规范中的相关内容:
决议程序处理是以一个promise和一个value为输入的抽象操作,我们把它表示为
[[Resolve]](promise, x)
别懵。这种形式看起来太高级了一点也不友好,但这种写法你肯定见过
promise2 = promise1.then(onResolved, onRejected);
[[Resolve]](promise, x)
意思是说如果 onResolved 或 onRejected 返回了值 x, 则执行 Promise 解析流程 [[Resolve]](promise2, x)
。
只要都实现了promise/A+标准,那么不同的Promise都可以之间相互调用。
-
如果 x 和 promise 都指向同一个对象,则以 typeError 为 reason 拒绝执行 promise。
-
如果 x 是 Promise 对象,则 promise 采用 x 当前的状态:
a. 如果 x 是 pending 状态,promise 必须保持 pending 状态直到 x 的状态变为 fulfilled 或者rejected
b. 如果 x 是 fulfilled 状态,用相同的值 value 执行 promise
c. 如果 x 是 rejected 状态,则用相同的 reason 执行 promise。
-
如果 x 是一个对象或者函数:
a. 将 promise 的 then 方法指向 x.then
b. 如果 x.then 属性抛出异常 error,则以 error 为 reason 来调用 reject
c. 如果 then 是是一个函数,那么用 x 为 this 来调用它,第一个参数为 resolvePromise,第二个参数为 rejectPromise
i. 如果 resolvePromise 以值 y 为参数被调用,则运行`[[Resolve]](promise, y)` ii. 如果 rejectPromise 以据因原因 r 为参数被调用,则用原因 r 执行 `promise(reject)` iii. 如果 resolvePromise 和 rejectPromise 均被调用,或者被同一参数调用了多次,则使用第一次调用并忽略剩下的调用 iv. 如果调用 then 抛出了异常 error (1). 如果 resolvePromise 或 rejectPromise 已经被调用,则忽略它。 (2). 否则用 error 为 reason 拒绝 promised。如果 then 不是 function,用 x 为参数执行 promise
-
如果 x 不是一个 object 或者 function,用 x 为参数执行promise
用决议程序完善 CutePromise
咱们主要的思路在于把上述的决议程序的逻辑给提出来,在此基础上完善 then 方法(因为决议程序我们会放到 then 方法里来调用)。
构造函数改造
构造函数侧的改造无需太多,我们主要是把 setTimeout 给拿掉。这是因为后续我们会把异步处理放到 then 方法中的 resolveByStatus/ rejectByStatus 里面来做。
function MyPromise(executor) {
// value 记录异步任务成功的执行结果
this.value = null;
// reason 记录异步任务失败的原因
this.reason = null;
// status 记录当前状态,初始化是 pending
this.status = 'pending';
// 缓存两个队列,维护 fulfilled 和 rejected 各自对应的处理函数
this.onResolvedQueue = [];
this.onRejectedQueue = [];
// 把 this 存下来,后面会用到
var self = this;
// 定义 resolve 函数
function resolve(value) {
// 如果是 pending 状态,直接返回
if (self.status !== 'pending') {
return;
}
// 异步任务成功,把结果赋值给 value
self.value = value;
// 当前状态切换为 fulfilled
self.status = 'fulfilled';
// 批量执行 fulfilled 队列里的任务
self.onResolvedQueue.forEach(resolved => resolved(self.value));
}
// 定义 reject 函数
function reject(reason) {
// 如果是 pending 状态,直接返回
if (self.status !== 'pending') {
return;
}
// 异步任务失败,把结果赋值给 value
self.reason = reason;
// 当前状态切换为 rejected
self.status = 'rejected';
// 批量执行 rejected 队列里的任务
self.onRejectedQueue.forEach(rejected => rejected(self.reason));
}
// 把 resolve 和 reject 能力赋予执行器
executor(resolve, reject);
}
下面我们来编写决议程序!这个 resolutionProcedure 可以说是咱们这节的一个学习的关键,各位留心阅读逐行注释中的解析
function resolutionProcedure(promise2, x, resolve, reject) {
// 这里 hasCalled 是个标识,是为了确保 resolve、reject 不要被重复执行
let hasCalled;
if (x === promise2) {
// 决议程序规范:如果 resolve 结果和 promise2 相同则 reject,这是为了避免死循环
return reject(new TypeError('为避免死循环,此处抛错'));
} else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
// 决议程序规范:如果 x 是一个对象或者函数,则需要额外处理下
try {
// 首先是看它有没有 then 方法(是不是 thenable 对象)
let then = x.then;
// 如果是 thenable 对象,则将 promise 的 then 方法指向 x.then。
if (typeof then === 'function') {
// 如果 then 是一个函数,那么用 x 为 this 来调用它
// 第一个参数为 resolvePromise,第二个参数为 rejectPromise
then.call(x, y => {
// 如果已经被 resolve/reject 过了,那么直接 return
if (hasCalled) return;
hasCalled = true;
// 进入决议程序(递归调用自身)
resolutionProcedure(promise2, y, resolve, reject);
}, err => {
// 这里 hascalled 用法和上面意思一样
if (hasCalled) return;
hasCalled = true;
reject(err);
});
} else {
// 如果 then 不是 function,用 x 为参数执行 promise
resolve(x);
}
} catch (e) {
if (hasCalled) return;
hasCalled = true;
reject(e);
}
} else {
// 如果 x 不是一个 object 或者 function ,用 x 为参数执行 promise
resolve(x);
}
}
这个决议程序会在 then 方法中被调用( then 方法同样伴随不小改动,大家留心注释解析)
// then 方法接收两个函数作为入参(可选)
MyPromise.prototype.then = function(onResolved, onRejected) {
// 注意,onResolved 和 onRejected必须是函数;如果不是,我们此处用一个透传来兜底
if (typeof onResolved !== 'function') {
onResolved = function(x) {return x};
}
if (typeof onRejected !== 'function') {
onRejected = function(e) {throw e};
}
// 依然是保存 this
var self = this;
// 这个变量用来存返回值 x
let x;
// resolve态的处理函数
function resolveByStatus(resolve, reject) {
// 包装成异步任务,确保决议程序在 then 后执行
setTimeout(function() {
try {
// 返回值赋值给 x
x = onResolved(self.value);
// 进入决议程序
resolutionProcedure(promise2, x, resolve, reject);
} catch (e) {
// 如果 onResolved 或者 onRejected 抛出异常 error
// 则 promise2 必须被 rejected,用 error 做 reason
reject(e);
}
});
}
// reject态的处理函数
function rejectByStatus(resolve, reject) {
// 包装成异步任务,确保决议程序在 then 后执行
setTimeout(function() {
try {
// 返回值赋值给 x
x = onRejected(self.reason);
// 进入决议程序
resolutionProcedure(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}
// 注意,这里我们不能再简单粗暴 return this 了,需要 return 一个符合规范的 Promise 对象
var promise2 = new MyPromise(function(resolve, reject) {
// 判断状态,分配对应的处理函数
if (self.status === 'fulfilled') {
// resolve 处理函数
resolveByStatus(resolve, reject);
} else if (self.status === 'rejected') {
// reject 处理函数
rejectByStatus(resolve, reject);
} else if (self.status === 'pending') {
// 若是 pending ,则将任务推入对应队列
self.onResolvedQueue.push(function() {
resolveByStatus(resolve, reject);
});
self.onRejectedQueue.push(function() {
rejectByStatus(resolve, reject);
});
}
});
// 把包装好的 promise2 return 掉
return promise2;
};
如此一来,我们就实现了一个符合预期的 Promsie 了,它可以通过 这套 Promise/A+ 规范的测试用例。
小建议
手写 Promise,在不同的面试官、不同的团队里,有着不同的答题标准。对一些团队来说,完成到我们没有加 Promise/A+ 规范时那种程度,已经可以拿到全部的分数。如果你是第一次接触 Promise 底层原理,同时在阅读本文的过程中感到吃力,这是非常正常的事情。不必心急,如果时间充裕,试着去多读几遍、一行一行跟着敲下来。