持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第9天,点击查看活动详情
前言
对于学习 promise,好多人不太喜欢看 官方文档,在这篇文章中,作者带你深挖 《Promises/A+ 规范》的内容,通过 解析官方文档 和 引导的方式与你一起手写 promise 源码
你将会收获到 👇:
promise更 深层次 的内容- 在 同步代码 中怎么处理 异步任务
- 高级函数 设计技巧
- 最重要的是将 异步等待、错误处理、链式调用 融入到开发思想里去
开始
在官方文档的 2.1.节 中指定 promise 必须处于 fulfilled(满足),rejected(拒绝),pending(等待) 三种状态
- 2.1.1.:处于
pending状态的可能会转变成其他两种状态 - 2.1.2.:处于
fulfilled状态不能转变为其他状态,而且必须有一个不可以转变的value - 2.1.3.:处于
rejected状态不能转变为其他状态,而且必须有一个不可以转变的reason
这非常好理解,pending 状态的 promise 需要通过 resolve(value) 才能转变成 fulfilled或者 reject(reason) 转变成 rejected
当我们 new Promise(executor) 实际上是传入一个 执行器 函数,这个执行器函数接收两个参数 resolve, reject,而且是同步执行,不难写出如下代码:
// 使用常量维护 promise 状态
const STATUS = {
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected',
};
class MyPromise { // 为了避免命名冲突,使用 MyPromise
status = STATUS.PENDING;
value;
reason;
constructor(executor) {
const resolve = value => {
if (this.status === STATUS.PENDING) { // 对应 2.1.2
this.status = STATUS.FULFILLED;
this.value = value;
}
};
const reject = reason => {
if (this.status === STATUS.PENDING) { // 对应 2.1.3
this.status = STATUS.REJECTED;
this.reason = reason;
}
};
executor(resolve, reject);
}
}
then
基本结构
在官方文档的 2.2. 节 中指出:一个 promise 必须指定一种方法访问当前或最终的 value 或者 reason,也就是说 只能访问 fulfilled 和 rejected 状态的 promise,如果状态为 pending 会 阻塞 Promise 链条
then 方法接收两个可选参数:
promise.then(onFulfilled, onRejected)
- 2.2.1.:如果两个参数不是
function,则会被忽略 - 2.2.2.:
onFulfilled如果是function,必须在promise变成fulfilled状态之后调用,value作为这个函数的第一个参数。onFulfilled不能在变成fulfilled状态之前被调用,并且不能被多次调用 - 2.2.3.:
onRejected的要求和onFulfilled差不多一致,使用reason作为函数的参数,必须在rejected状态之后被调用一次
then(onFulfilled, onRejected) {
// 对应 2.2.1. 也满足 2.2.7.3
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => reason;
// 对应 2.2.2. 也满足 2.2.7.4
if (this.status === STATUS.FULFILLED) {
onFulfilled(this.value);
}
// 对应 2.2.3.
if (this.status === STATUS.REJECTED) {
onRejected(this.reason);
}
}
这时候我们就完成了 Promise 的雏形,测试一下:
new MyPromise((resolve, reject) => {
resolve(1);
// reject(2);
}).then(
value => {
console.log(value);
},
reason => {
console.log(reason);
}
);
不论是 resolve 还是 reject 都能在 then 里被正确输出
总结:我们在构造函数中使用
status维护MyPromise的状态,根据状态的不同 分发 对应的处理过程,在then方法中依然根据状态的不同调用对应状态的函数参数
处理异步
如果我们在 MyPromise 的异步任务中执行 resolve,程序不执行输出
new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(1);
});
}).then(
value => {
console.log(value);
},
reason => {
console.log(reason);
}
);
因为 resolve 或者 reject 一旦被包裹在 异步任务 中,同步执行 到 then 代码的时候, promise 的还是 pending,所有我们得在 then 方法里面处理 pending 的情况
class MyPromise {
// ...
onFulfilledCbs = []; // 收集 pending 状态 未执行 onFulfilled
onRejectedCbs = []; // 收集 pending 状态 未执行 onRejected
constructor(executor) {
const resolve = value => {
if (this.status === STATUS.PENDING) {
// ...
this.onFulfilledCbs.forEach(fn => fn()); // 等到异步任务完成后发布
}
};
const reject = reason => {
if (this.status === STATUS.PENDING) {
// ...
this.onRejectedCbs.forEach(fn => fn()); // 等到异步任务完成后发布
}
};
// ...
}
then(onFulfilled, onRejected) {
if (this.status === STATUS.PENDING) {
this.onFulfilledCbs.push(() => { // 先订阅
onFulfilled(this.value);
});
this.onRejectedCbs.push(() => { // 先订阅
onRejected(this.reason);
});
}
}
}
我们在 同步执行到 then 方法的时候,如果 promise 的 状态为 pending,我们将用一层函数包裹 回调函数 放到收集器里,等到 异步任务 完成后,resolve / reject 会通过 闭包 访问到构造函数里的变量,执行后续的代码
异步 then
在官方文档的 2.2.4. 节指出,promise.then 方法执行必须等到 上下文执行栈 只包含 平台代码,这里说的很难懂,好在 3.1. 节对 平台代码 进行说明:
翻译 / (解读):
平台代码 说的是实现 promise 的引擎,环境(比如 浏览器,node...)。在实践中,这样可以确保在事件循环的 轮询阶段 (借用 node 事件循环的概念)异步调用代码(promise.then 方法是异步执行的,必须要等到同步任务执行完才能开启堆栈执行),并使用新的堆栈。可以用例如 setTimeout、setInterval 这样的 宏任务,也可以使用 MutationObserver(h5 新概念)、process.nextTick(node 里面的内容)这样的微任务实现上文所说的异步。因为 Promise 的实现被当做平台代码,因此它本身可能包含调用处理程序的任务调度队列(宏任务或者微任务)或“蹦床”)。
官方解释最后一句可能想表达这样的意思:
const promise = Promise.resolve(1);
promise.then(value => {
console.log(value, 1);
promise.then(value => {
console.log(value, 2);
});
});
// 1 1
// 1 2
对于同一个 promise 可以在 promise.then 里面嵌套使用。而且多个 then 接收到的 value 和 reason 都是全等的
根据上文我们可以将 MyPromise 的代码改造成
then(onFulfilled, onRejected) {
// ...
if (this.status === STATUS.FULFILLED) {
setTimeout(() => {
onFulfilled(this.value);
});
}
if (this.status === STATUS.REJECTED) {
setTimeout(() => {
onRejected(this.reason);
});
}
if (this.status === STATUS.PENDING) {
this.onFulfilledCbs.push(() => {
onFulfilled(this.value);
});
this.onRejectedCbs.push(() => {
onRejected(this.reason);
});
}
}
我们在 then 里对 promise 状态分发到对应的处理函数之后,立即进行 异步处理,使用 定时器 这个 宏任务 包内部程序(对于官方要求 2.2.4.),在 v8 引擎 底层是用 c++ 写的 微任务,所以这里我们很难做到一模一样
为什么 pending 状态的
promise不用异步代码包裹:因为then中判断pending里面的内容一定会执行。如果new Promise中没有调用resolve / reject,onFulfilledCbs或者onRejectedCbs只有收集的函数,但没有机会调用;如果异步调用resolve / reject,再异步调用then中的pending,可能造成then后于resolve / reject执行,此时收集器内并无onFulfilled / onRejcted
2.2.5. onFulfilled 必须作为作为函数,尽管没有值
2.2.6. then 可以在同一个 promise 多次调用,不管是 rejected 还是 fulfilled 状态,各自的回调函数按照初始的顺序执行
这两个要求我们在之前的代码已经达到了
2.2.7. then 必须返回一个 promise
promise2 = promise1.then(onFulfilled, onRejected);
说明 then 返回的 promise2 与之前的 promise1 不是同一个 promise
2.2.7.1:如果 onFulfilled / onRejected 返回一个值 x ,请运行 解析Promise程序 解析 x,在官方文档 3.1. 节会有解释,下文也会详细讲解这个过程
2.2.7.2.:如果 onFulfilled / onRejected 抛出一个不可以预期的错误 e ,promise2 请 reject 这个 reason
所以按照这两个要求继续改造 MyPromise.prototype.then:
then(onFulfilled, onRejected) {
// ...
const promise2 = new MyPromise((resolve, reject) => {
if (this.status === STATUS.FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolveX(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}
if (this.status === STATUS.REJECTED) {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolveX(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}
if (this.status === STATUS.PENDING) {
this.onFulfilledCbs.push(() => {
try {
let x = onFulfilled(this.value);
resolveX(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
this.onRejectedCbs.push(() => {
try {
let x = onRejected(this.reason);
resolveX(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}
});
return promise2; // 返回的 promise2 可以调用 then
}
function resolveX(promise2, x, resolve, reject) {
resolve(x); // 这里我们简单处理了一下 x 的值,让 x 可以继续被传递
}
2.2.7.3:如果 onFulfilled 不是函数并且 promise1 的状态时 fulfilled, promise2 必须以与 promise1 相同的 value 实现。
2.2.7.4:如果 onRejected 不是函数并且 promise1 的状态时 rejected, promise2 必须以与 promise1 相同的 reason 实现。
这两个要求说的是:可以多个 then 穿透传递 value / reason,这里就是 链式调用 的核心实现步骤了
Promise.resolve(1)
.then()
.then()
.then()
.then(value => console.log(value));
这两个要求其实在之前我们已经实现了,如果 then 的两个参数不是函数的话,promise 的 value / reason 会继续传递,我们只要给这两个参数添加上默认值即可。
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => reason;
// ...
}
到此,整个 then 方法我们基本就还原完毕了。
测试一下:
new MyPromise((resolve, reject) => {
// setTimeout(() => {
resolve(1);
// });
})
.then()
.then()
.then()
.then(
value => {
console.log(value, 'value');
},
reason => {
console.log(reason, 'reason');
}
);
resolve 正常,如果我们将 resolve 换成 reject,依然会输出成 value,而不是 reason,这是为什么呢?
重点:
new MyPromise(executor)的executor执行器会先执行resolve,一旦状态被修改为fulfilled,就不会执行rejected
解决方案:我们可以在 resolveX(promise2, x, resolve, reject, promise1) 多增加一个参数,这个参数在调用的时候只需要在 then 中将 this 传入,因为 this 就是 promise1,我们可以根据 promise1 的状态修改 promise2 的状态
这种做法不如 promise 原生手法来的精妙,首先我们的解决方案 违背了开闭原则:resolveX 函数应该只处理 x,尽量不修改 promise2,其次我们处理过程 不能很好地区分 fulfilled 和 rejected
在 v8 引擎 底层是利用错误机制处理这个问题:
class MyPromise {
constructor (executor) {
// ...
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
// ...
}
then (onFulfilled, onRejected) {
onRejected =
typeof onRejected === 'function'
? onRejected
: reason => {
throw reason;
};
}
}
在调用 onRejected 函数会报错,同时在执行 MyPromise 构造函数的时候接收 reason,是不是很巧妙呢?
resolveX
在官方文档的 2.3 节的内容规定 resolveX 函数的实现
不知道大家思考过:为什么会有
resolveX的过程?
因为 x 的返回值也可能是个 promise,而且这个 promise 经过 Promise.resolve() 包装后并抛出,在这个专栏的《其实你不知道 Promise.then - 掘金 (juejin.cn)》,我们详细地讲解了这个过程
Promise.resolve(1)
.then(() => {
return Promise.resolve(2);
})
.then(value => {
console.log(value); // 2
});
来源于:Promises/A+ (promisesaplus.com)
这段话大概说的是也不一定要按照 2.3. 节的要求实现 then,v8 引擎 底层依然加强了这个规范的要求
2.3.1.:如果 x 和 promise 引用同一对象,则 reject promise以 TypeError 作为原因拒绝
2.3.2.:如果 x 是 promise,则要维护它的 状态 和 value/reason
2.3.3:如果 x 是个函数或对象:
- 2.3.3.1:让
then = x.then - 2.3.3.2:如果 x.then 导致抛出异常
e(比如发生数据劫持),reject promise,reason是e。 - 2.3.3.3:然后是一个函数,用
x调用它,第一个参数resolvePromise,第二个参数rejectPromise,(这里是处理thenable接口,如果不了解thenable可以阅读《你真的明白 promise 的 then 吗?》) - 2.3.3.4:如果不是函数,则将
x设置为fulfilled状态
2.3.4.:如果 x 不是 function / object,则将 x 设置为 fulfilled 状态
根据 2.3 节的要求我们能写出如下代码:这里的代码逻辑完全是与官方文档的要求同步
function resolveX(promise2, x, resolve, reject) {
// 对应 2.3.3.1
if (x === promise2) {
return reject(new TypeError('Chaining cycle detected for promise #<MyPromise>'));
}
// 对应 2.3.3.2
// 处理 Promise
if (x instanceof MyPromise) {
if (x.status === 'pending') {
// 对应 2.3.3.2.1
x.then(function (v) {
resolveX(promise2, v, resolve, reject);
}, reject);
} else {
// 对应 2.3.3.2.2 和 2.3.3.2.3
x.then(resolve, reject);
}
}
// 对应 2.3.3.3
// 处理 thenable
let called = false; // 对应 2.3.3.3.3 resolvePromise 和 rejectPromise 只能被调用一次
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
then = x.then;
if (typeof then === 'function') {
then.call(
x,
function resolvePromise(y) {
// 对应 2.3.3.3.1
if (called) return; // 对应 2.3.3.3.3
called = true;
return resolveX(promise2, y, resolve, reject);
},
function rejectPromise(r) {
// 对应 2.3.3.3.2
if (called) return; // 对应 2.3.3.3.3
called = true;
return reject(r);
}
);
} else {
// 对应 2.3.3.4
resolve(x);
}
} catch (e) {
// 对应 2.3.3.3.4
if (called) return; // 对应 2.3.3.3.3
called = true;
return reject(e);
}
} else {
// 对应 2.3.4
resolve(x);
}
}
测试案例:
const p = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(1);
});
})
.then(value => {
console.log(value, 'value'); // 1
return new MyPromise((resolve, reject) => {
resolve(2);
});
})
.then(value => {
console.log(value); // 2
});
const p = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(1);
});
})
.then(value => {
console.log(value, 'value'); // 1
return {
then(resolve, reject) {
reject(2);
},
};
})
.then(
value => {
console.log(value, 'value');
},
reason => {
console.log(reason, 'reason'); // 2
}
);
至此,关于 promise 的源码已经完全还原了,除了在 3.1 节我们无法使用微任务包裹对应的处理程序,其他的部分我们都完全按照 《Promises/A+ 规范》的要求实现了
catch
catch 方法就比较简单了,本质就是一个简化版的 then
catch(onRejected){
return this.then(null, onRejected);
}
总结
手写 promise 源码有很多值得学习的地方,例如处理怎么 处理异步?怎么利用 错误机制 优化代码?如何将 异步等待、链式调用 的思想融入到你的代码里,熟练地使用 高阶函数 封装 底层库
如果觉得本文不错的话,可以给作者点一个小小的赞,你的鼓励将是我前进的动力
如果你本文对你有帮助或者你有不同的意见,欢迎在评论区留下你的足迹
Promise 相关文章推荐
-
promise 中的细节:
《其实你不知道 Promise.then 》
《你真的明白 promise 的 then 吗?》
《await 中那些不为人知的细节》 -
专栏:
《从 v8 看 promise》