# 实现一个完全符合规范的Promise
从网上看了太多名为《手把手教你写个Promise
》的教程,内容大同小异。
几乎都有一个缺点,没有把微任务加上,都是用setTimeout
处理的异步,虽然Promise A+
规范 (opens new window)并没有要求这个,但像浏览器、nodejs
都把Promise
当作微任务中的一种,所以我们需要模拟下。
此节暂时按下不表。
我们一点点来。
# 简单实现
先看下promise
的例子:
aa = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'ok');
});
aa.then((res) => console.log(res));
bb = new Promise((resolve, reject) => {
setTimeout(reject, 100, 'error');
});
bb.catch((err) => console.error(err));
Promise.resolve(123).then(function(){
return Promise.resolve(456);
}).then(function(res){
console.log(res); //打印456
});
一般来说,我们只需要知道以下几点,就差不多了。
Promise
构造函数的参数是个函数,这个函数又显式地要求有2个参数resolve
和reject
方法,分别对应成功或者失败的函数每个
Promise
实例是可以用.then
或者.catch
链式调用,而每次调用,其实都不是它自己了,都会返回一个新的Promise
实例.then
方法有2个函数作为参数,分别对应成功处理和失败处理一个实例有3种状态(进行中(
pending
)、成功(fulfilled
)、失败(rejected
)),成功或失败后就不能再更改状态了
参见以下这种情况,先resolve了,但后面发生异常,依然是成功状态。
new Promise((resolve, reject) => {
resolve('111');
throw new Error('err');
})
}.then(function(){
console.log('--ok--'); //打印此句
}).catch(function(){
console.log('-------err--')
});
其细节颇多,要完整符合规范并不容易,我这里也先写个简单的。
在写之前,我们需要思考下难点在哪里。
我们知道,Promise
的出现,是为了解决前端异步回调的痛点,它的每次.then
,都是异步的操作。但在js
里,我们.then
的时候,其实是执行了一个对象的then
方法,这是个同步的操作。
也就是说,then
方法中的代码,肯定不能立即执行,必须得缓存起来,在合适的时机再执行它。
而什么时候执行呢?代码中显式调用resolve
或reject
以后,就可以执行了。
一般来说,有2种延时方案:
一种是在resolve
或者reject
执行时,进行延时;
一种是在.then
方法里,执行对应的函数时进行延时。
2种都能实现大致功能,但前者在完全符合规范编写过程中遇坑不少,这里我们选择第二种。
# 步骤一:初始化构造函数
先定义3种状态:
const PENDING = 'pending'; //进行中
const FULFILLED = 'fulfilled'; //成功
const REJECTED = 'rejected'; //失败
再写构造函数,内置3个属性——状态status
、成功返回值value
、失败原因reason
。把resolve
和reject
这两个内置函数作为fn
的参数执行,它俩的责任就是改变当前promise
的状态值。
class Promise {
constructor(fn) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
const resolve = (val) => {
if (this.status !== PENDING) { // 参见第4条,状态变化后就不能再修改了
return;
}
this.status = FULFILLED;
this.value = val;
//todo
};
const reject = (err) => {
if (this.status !== PENDING) { // 参见第4条,状态变化后就不能再修改了。
return;
}
this.status = REJECTED;
this.reason = err;
//todo
};
try {
fn(resolve, reject);
} catch (e) {
reject(e);
}
}
}
# 步骤二:实现then方法
从第2条和第3条可知,then
方法需要返回一个新的Promise
,且要异步处理。怎么异步处理呢?上面提到,需要把then
的这两个参数缓存起来,于是我们把它们分别存到2个数组中。
为什么用数组,而不是普通变量呢?我开始也困惑了好久,后来想到,有这样一种使用场景:
a = Promise.resolve(123);
a.then(console.log);
a.then(console.error);
上述一个promise
会在不同的地方.then
执行,显然,两个回调函数应该一前一后执行,而不是丢失某一个。
所以,我们在构造函数中添加2个数组:
constructor(fn) {
...
this.onFulfilledList = [];
this.onRejectedList = [];
...
}
继而在then
中,在状态为pending
时,把需要延时处理的函数推送到这俩数组中。
需要注意的是,对于then
传递的这俩参数,如果在需要的时候它不是函数,则会忽略不计,把当前promise
的状态传递到下一个。
then(onFulfilled, onRejected) {
return new Promise((resolve, reject) => {
const onResolvedFunc = function (val) {
const cb = function () {
try {
if (typeof onFulfilled !== 'function') { // 如果成功了,它不是个函数,意味着不能处理,则把当前Promise的状态继续向后传递
resolve(val);
return;
}
const x = onFulfilled(val);
resolve(x);
} catch (e) {
reject(e);
}
};
setTimeout(cb, 0);
};
const onRejectedFunc = function (err) {
const cb = function () {
try {
if (typeof onRejected !== 'function') { // 如果失败了,它不是个函数,意味着不能处理,则把当前Promise的状态继续向后传递
reject(err);
return;
}
const x = onRejected(err);
resolve(x); //处理了失败,则意味着要返回的新的promise状态是成功的
} catch (e) {
reject(e);
}
};
setTimeout(cb, 0);
};
if (this.status === PENDING) {
this.onFulfilledList.push(onResolvedFunc);
this.onRejectedList.push(onRejectedFunc);
} else if (this.status === FULFILLED) {
//todo
} else {
//todo
}
});
}
下来就是在构造函数的resolve
和reject
中执行我们缓存的函数。
constructor(fn) {
...
const resolve = (val) => {
...
this.onFulfilledList.forEach((cb) => cb && cb.call(this, val));
this.onFulfilledList = [];
};
const reject = (err) => {
...
this.onRejectedList.forEach((cb) => cb && cb.call(this, err));
this.onRejectedList = [];
};
...
}
现在,下面的例子应该可以成功了:
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('111');
}, 0);
}).then(function () {
console.log('--ok--')
});
但去掉延时(setTimeout
)就会失败。分析下原因就明白了,我们在调用resolve
时就改变了status
,走到then
方法时,状态已经变为成功了。所以我们需要在状态已经变为成功或失败时,直接调用回调函数,而不是依赖resolve
或reject
触发:
then(onFulfilled, onRejected) {
...
if (this.status === PENDING) {
...
} else if (this.status === FULFILLED) {
onResolvedFunc(this.value);
} else { // if(this.status === REJECTED) { //如果这个Promise已经失败,说明已经reject过了,不能再依赖reject来触发,就直接执行失败处理。
onRejectedFunc(this.reason);
}
}
这样,下面的代码就ok
了:
new Promise((resolve, reject) => {
resolve('111');
}).then(function () {
console.log('--ok--')
});
# 步骤三:实现catch
catch
的实现很简单,就是个then
的语法糖:
catch(onRejected) {
return this.then(null, onRejected);
}
# 步骤四:模拟微任务
按理说promise
的主要功能已经实现了,但下面的代码就会暴露一个问题:
setTimeout(function(){
console.log('---timeout--')
});
new Promise((resolve, reject) => {
resolve('111');
}).then(function () {
console.log('--ok--')
});
console.log('--log--');
会先打印log
,这是正常的,再打印timeout
,最后打印ok
。这就违反了微任务优先的原则,毕竟它是VIP
啊。
怎么做呢?
浏览器中有个MutationObserver
,nodejs
中可以使用process.nextTick
,这俩都没有的时候,再回退到setTimeout
。这才是正确的实现姿势。
为了图方便,我照抄了vue
的nextTick
中的代码,具体可见这里。
const isInBrowser = typeof window !== 'undefined';
const nextTick = function(nextTickHandler) {
if (isInBrowser) {
if (typeof MutationObserver !== 'undefined') { // 首选 MutationObserver
var counter = 1;
var observer = new MutationObserver(nextTickHandler); // 声明 MO 和回调函数
var textNode = document.createTextNode(counter);
observer.observe(textNode, { // 监听 textNode 这个文本节点
characterData: true // 一旦文本改变则触发回调函数 nextTickHandler
});
const start = function () {
counter = (counter + 1) % 2; // 每次执行 timeFunc 都会让文本在 1 和 0 间切换
textNode.data = counter;
};
start();
} else {
setTimeout(nextTickHandler, 0);
}
} else {
process.nextTick(nextTickHandler);
}
};
再把then
方法中setTimeout
换成nextTick
就可以了。
# 完整实现
怎么能完全符合规范呢?有个检查工具:promises-aplus-tests
, 在npm
中安装一下,再在script
标签中使用:
"test": "promises-aplus-tests src/promise.js --reporter spec"
需要你的promise.js
用module.exports
导出,再在内部实现一个deferred
方法:
static deferred() {
let result = {};
result.promise = new Promise((resolve, reject) => {
result.resolve = resolve;
result.reject = reject;
});
return result;
}
接下来就能执行所有的测试用例了。
还有些额外的方法实现,不在这里展现了,可以看这篇。
具体细节就不再补充了。以下是完整代码实现:
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
const isInBrowser = typeof window !== 'undefined';
class Promise {
static isPromise(val) {
return val && val instanceof Promise;
}
static resolve(val) {
if (Promise.isPromise(val)) {
return val;
}
return new Promise((resolve) => {
if (val && typeof val.then === 'function') { // 如果返回值是个thenable对象,需要处理下
val.then((res) => {
resolve(res);
});
return;
}
resolve(val);
});
}
static reject(val) {
//reject不区分是不是Promise
return new Promise((_, reject) => {
reject(val);
});
}
/**
* 用MutationObserver生成浏览器的nextTick,nodejs端则用process.nextTick
*/
static nextTick(nextTickHandler) {
if (isInBrowser) {
if (typeof MutationObserver !== 'undefined') { // 首选 MutationObserver
var counter = 1;
var observer = new MutationObserver(nextTickHandler); // 声明 MO 和回调函数
var textNode = document.createTextNode(counter);
observer.observe(textNode, { // 监听 textNode 这个文本节点
characterData: true // 一旦文本改变则触发回调函数 nextTickHandler
});
const start = function () {
counter = (counter + 1) % 2; // 每次执行 timeFunc 都会让文本在 1 和 0 间切换
textNode.data = counter;
};
start();
} else {
setTimeout(nextTickHandler, 0);
}
} else {
process.nextTick(nextTickHandler);
}
}
static all(arr) {
if (!Array.isArray(arr)) {
throw new TypeError('undefined is not iterable.');
}
let count = arr.length;
const result = [];
if (count === 0) {
return Promise.resolve(result);
}
return new Promise((resolve, reject) => {
Promise.resolve(promise).then((res) => {
count--;
result[i] = res;
if (count === 0) {
resolve(result);
}
}, reject);
});
}
static allSettled(arr) {
if (!Array.isArray(arr)) {
throw new TypeError('undefined is not iterable.');
}
let count = arr.length;
const result = [];
if (count === 0) {
return Promise.resolve(result);
}
return new Promise((resolve) => {
arr.forEach((promise, i) => {
Promise.resolve(promise).then((res) => {
count--;
result[i] = {
value: res,
status: FULFILLED
};
if (count === 0) {
resolve(result);
}
}, (err) => {
count--;
result[i] = {
reason: err,
status: REJECTED
};
if (count === 0) {
resolve(result);
}
});
});
});
}
static race(arr) {
if (!Array.isArray(arr)) {
throw new TypeError('undefined is not iterable.');
}
let count = arr.length;
if (count === 0) {
return new Promise(() => { }); //返回一个永远pending的promise,这是跟all等不一样的地方
}
return new Promise((resolve, reject) => {
arr.forEach((promise) => {
Promise.resolve(promise).then(resolve, reject);
});
});
}
static deferred() {
let result = {};
result.promise = new Promise((resolve, reject) => {
result.resolve = resolve;
result.reject = reject;
});
return result;
}
/**
* 先执行同步代码,替代Promise.resolve().then(func)
* @example
*
* const f = () => console.log('now');
* Promise.try(f);
* console.log('next');
*/
static try(func){
return new Promise((resolve) => {
resolve(func());
});
}
constructor(fn) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.onFulfilledList = [];
this.onRejectedList = [];
try {
fn(this.handleResolve.bind(this), this.handleReject.bind(this));
} catch (e) {
this.handleReject(e);
}
}
/**
* 处理成功
* 就是我们使用的resolve函数
* @param val 成功参数
*/
handleResolve(val) {
if (this.status !== PENDING) {
return;
}
this.status = FULFILLED;
this.value = val;
this.onFulfilledList.forEach((cb) => cb && cb.call(this, val));
this.onFulfilledList = [];
}
/**
* 处理失败
* 就是我们使用的reject函数
* @param err 失败信息
*/
handleReject(err) {
if (this.status !== PENDING) {
return;
}
this.status = REJECTED;
this.reason = err;
this.onRejectedList.forEach((cb) => cb && cb.call(this, err));
this.onRejectedList = [];
}
then(onFulfilled, onRejected) {
const promise2 = new Promise((resolve, reject) => {
const resolvePromise = function (x) {
if (x === promise2) {
reject(new TypeError('The promise and the return value are the same'));
return;
}
if (x && typeof x === 'object' || typeof x === 'function') {
let used; //PromiseA+2.3.3.3.3 只能调用一次
try {
let then = x.then;
if (typeof then === 'function') {
//PromiseA+2.3.3
then.call(x, (y) => {
//PromiseA+2.3.3.1
if (used) return;
used = true;
resolvePromise(y);
}, (r) => {
//PromiseA+2.3.3.2
if (used) return;
used = true;
reject(r);
});
} else {
//PromiseA+2.3.3.4
if (used) return;
used = true;
resolve(x);
}
} catch (e) {
//PromiseA+ 2.3.3.2
if (used) return;
used = true;
reject(e);
}
} else {
//PromiseA+ 2.3.3.4
resolve(x);
}
};
const onResolvedFunc = function (val) {
var cb = function () {
try {
if (typeof onFulfilled !== 'function') { // 如果成功了,它不是个函数,意味着不能处理,则把当前Promise的状态继续向后传递
resolve(val);
return;
}
const x = onFulfilled(val);
resolvePromise(x);
} catch (e) {
reject(e);
}
};
Promise.nextTick(cb);
};
const onRejectedFunc = function (val) {
var cb = function () {
try {
if (typeof onRejected !== 'function') { // 如果失败了,它不是个函数,意味着不能处理,则把当前Promise的状态继续向后传递
reject(val);
return;
}
const x = onRejected(val);
resolvePromise(x);
} catch (e) {
reject(e);
}
};
Promise.nextTick(cb);
};
if (this.status === PENDING) {
//这样把then注册的函数,放到list中延时执行。内部加了try/catch,把修改状态的逻辑全放在了handleResolve、handleReject这俩函数中
this.onFulfilledList.push(onResolvedFunc);
this.onRejectedList.push(onRejectedFunc);
} else if (this.status === FULFILLED) { //如果这个Promise已经成功,说明已经resolve过了,不能再依赖resolve来触发,就直接执行成功处理。比如aa = Promise.resolve(),有多处使用.then
onResolvedFunc(this.value);
} else { // if(this.status === REJECTED) { //如果这个Promise已经失败,说明已经reject过了,不能再依赖reject来触发,就直接执行失败处理。
onRejectedFunc(this.reason);
}
});
return promise2;
}
catch(onRejected) {
return this.then(null, onRejected);
}
finally(callback) {
return this.then(
(val) => Promise.resolve(callback()).then(() => val),
(err) => Promise.resolve(callback()).then(() => { throw err })
);
}
done(onFulfilled, onRejected) {
this.then(onFulfilled, onRejected)
.catch(function (reason) {
// 抛出一个全局错误
setTimeout(() => {
throw reason
}, 0);
});
}
toString() {
return '[object Promise]';
}
}
module.exports = Promise;