手写一个符合 Promise A+ 规范的 Promise
0. 相关文档
1. 准备工作
安装 promises-aplus-tests,编写测试代码。
npm init
npm install promises-aplus-tests -D
// src/index.js
const promisesAplusTests = require("promises-aplus-tests");
const adapter = require("./MyPromise") // 这里就是要自己写的Promise
promisesAplusTests(adapter, function (err) {
// All done; output is in the console. Or check `err` for number of failures.
});
这里查阅一下相关文档,发现这个adapter的定义为:
In order to test your promise library, you must expose a very minimal adapter interface. These are written as Node.js modules with a few well-known exports:
resolved(value): creates a promise that is resolved withvalue.
rejected(reason): creates a promise that is already rejected withreason.deferred(): creates an object consisting of { promise, resolve, reject }:
promiseis a promise that is currently in the pending state.resolve(value)resolves the promise withvalue.reject(reason)moves the promise from the pending state to the rejected state, with rejection reasonreason.The
resolvedandrejectedexports are actually optional, and will be automatically created by the test runner usingdeferredif they are not present. But, if your promise library has the capability to create already-resolved or already-rejected promises, then you should include these exports, so that the test runner can provide you with better code coverage and uncover any bugs in those methods.
因此在 MyPromise.js 中:
class MyPromise {
constructor(fn) {
fn(this.resolve, this.reject);
}
resolve() {...}
reject() {...}
}
function deferred() {
let dfd = {};
dfd.promise = new MyPromise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}
module.exports = { deferred };
这时在项目目录下运行:
node ./src/index.js
如果没有报错,会自动进入测试阶段,当然这时候我们肯定是一个测试用例都过不了。
2. 阅读规范
2.0 Do some Utils
'use strict';
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
const Utils = {
canChangeState: state => state === PENDING,
isObject: val => typeof val === 'object' && val !== null,
isFunction: val => typeof val === 'function',
isPromise: val => val instanceof MyPromise,
};
2.1 Promise States
A promise must be in one of three states: pending, fulfilled, or rejected.
When pending, a promise:
- may transition to either the fulfilled or rejected state.
When fulfilled, a promise:
- must not transition to any other state.
- must have a value, which must not change.
When rejected, a promise:
- must not transition to any other state.
- must have a reason, which must not change.
Here, “must not change” means immutable identity (i.e.
===), but does not imply deep immutability.
这里简单来说就是 Promise 有3个 state,状态一经改变就不能再变,这个实现非常简单。
class MyPromise {
state;
constructor(fn) {
this.state = PENDING;
fn(this.resolve, this.reject);
}
resolve(value) {
if(Utils.canChangeState(this.state)) this.state = FULFILLED;
}
reject(reason) {
if(Utils.canChangeState(this.state)) this.state = REJECTED;
}
then() {
}
}
2.2 thenmethod
A promise’s
thenmethod accepts two arguments:promise.then(onFulfilled, onRejected)
Both onFulfilled and onRejected are optional arguments:
- If
onFulfilledis not a function, it must be ignored.- If
onRejectedis not a function, it must be ignored.If onFulfilled is a function:
- it must be called after
promiseis fulfilled, withpromise’s value as its first argument.- it must not be called before
promiseis fulfilled.- it must not be called more than once.
If onRejected is a function,
- it must be called after
promiseis rejected, withpromise’s reason as its first argument.- it must not be called before
promiseis rejected.- it must not be called more than once.
onFulfilledoronRejectedmust not be called until the execution context stack contains only platform code. [3.1].
onFulfilledandonRejectedmust be called as functions (i.e. with nothisvalue). [3.2]then may be called multiple times on the same promise.
- If/when
promiseis fulfilled, all respectiveonFulfilledcallbacks must execute in the order of their originating calls tothen.- If/when
promiseis rejected, all respectiveonRejectedcallbacks must execute in the order of their originating calls tothen.
thenmust return a promise [3.3].promise2 = promise1.then(onFulfilled, onRejected);
- If either
onFulfilledoronRejectedreturns a valuex, run the Promise Resolution Procedure[[Resolve]](promise2, x).- If either
onFulfilledoronRejectedthrows an exceptione,promise2must be rejected witheas the reason.- If
onFulfilledis not a function andpromise1is fulfilled,promise2must be fulfilled with the same value aspromise1.- If
onRejectedis not a function andpromise1is rejected,promise2must be rejected with the same reason aspromise1.
这个 then 的规则比较多,一条条来看:
2.2.1
onFulfilled 和 onRejected两个参数都是可选参数,且两者必须是函数,不是则忽略
then(onFulfilled, onRejected) {
onFulfilled = Utils.isFuntion(onFulfilled) ? onFulfilled : undefined;
onRejected = Utils.isFuntion(onRejected) ? onRejected : undefined;
}
2.2.2
onFulfilled必须在fulfilled之后调用,且携带相应参数
value; // 决议成功的值
onFulfilledTasks = []; // 回调队列
resolve(value) {
this.state = FULFILLED;
this.value = value;
this.onFulfilledTasks.forEach(cb => cb(this.value)); // 循环遍历队列
}
2.2.3
onRejected必须在rejected之后调用,且携带相应参数
reason; // 决议失败的原因
onRejectedTasks = []; // 回调队列
reject(reason) {
this.state = REJECTED;
this.reason = reason;
this.onRejectedTasks.forEach(cb => cb(this.reason));
}
2.2.4
来说就是 onFulfilled 和 onRejected 必须异步调用
这里我的想法是在循环遍历回调队列的时候,异步执行每一个回调函数,用代码表述就是这样:
resolve(value) {
if (Utils.canChangeState(this.state)) {
this.state = FULFILLED;
this.value = value;
process.nextTick(()=>this.onFulfilledTasks.forEach(cb => cb(this.value)));
}
}
reject(reason) {
if (Utils.canChangeState(this.state)) {
this.state = REJECTED;
this.reason = reason;
process.nextTick(() => this.onRejectedTasks.forEach(cb => cb(this.reason)));
}
}
2.2.5
onFulfilled 和 onRejected被调用时无this,这里必须绑定到undefined,不能绑定到null,不然测试case会报错
then(onFulfilled, onRejected) {
onFulfilled = Utils.isFuntion(onFulfilled) ? onFulfilled.bind(undefined) : undefined;
onRejected = Utils.isFuntion(onRejected) ? onRejected.bind(undefined) : undefined;
}
2.2.6
同一个Promise,多个then注册必须按顺序执行多次回调
这里有个测试的case需要注意:
// 即使在 onFulfilled or onRejected 中调用then,也必须注册才行
promise.then(null, function () {
handler1();
// 这时onReject被调用的话,promise肯定已经处于Rejected决议了,因此在处理then的时候需要对此时的决议状态进行判断
promise.then(null, handler3);
});
一开始没有考虑到这种case的时候的代码:
then(onFulfilled, onRejected) {
onFulfilled = Utils.isFuntion(onFulfilled) ? onFulfilled.bind(undefined) : undefined;
onRejected = Utils.isFuntion(onRejected) ? onRejected.bind(undefined) : undefined;
onFulfilled && this.onFulfilledTasks.push(onFulfilled);
onRejected && this.onRejectedTasks.push(onRejected);
}
出现的问题:
const a = new MyPromise((res, rej)=>{
res();
})
a.then(()=>{
console.log(1);
a.then(()=>{
console.log(2);
})
})
// 输出: 1
// 这里仅仅只会打印一个 1
// 并不会打印 2
// 尽管第一个onFulfilled被加入了队列,但是在执行的时候,这时候a的状态已经是resolve了
// 尽管第二个onFulfilled也被加入了队列,但并不会再去遍历一边任务队列再去执行。
改进:
then(onFulfilled, onRejected) {
onFulfilled = Utils.isFuntion(onFulfilled) ? onFulfilled.bind(undefined) : undefined;
onRejected = Utils.isFuntion(onRejected) ? onRejected.bind(undefined) : undefined;
if (this.state === PENDING) {
onFulfilled && this.onFulfilledTasks.push(onFulfilled);
onRejected && this.onRejectedTasks.push(onRejected);
} else if (this.state === FULFILLED) {
process.nextTick(()=>{
onFulfilled();
})
} else if (this.state === REJECTED) {
process.nextTick(()=>{
onRejected();
})
}
}
2.2.7
then必须返回一个promise
这个要求其实是then方法中最难理解的一个规则,我也是琢磨了好半天才悟出其中真谛。
首先返回一个Promise,那我们先把大致框架给写好,再往里面填东西。
then(onFulfilled, onRejected) {
onFulfilled = Utils.isFuntion(onFulfilled) ? onFulfilled.bind(undefined) : undefined;
onRejected = Utils.isFuntion(onRejected) ? onRejected.bind(undefined) : undefined;
// 返回一个promise
return new MyPromise((res, rej) => {
if (this.state === PENDING) {
onFulfilled && this.onFulfilledTasks.push(onFulfilled);
onRejected && this.onRejectedTasks.push(onRejected);
} else if (this.state === FULFILLED) {
process.nextTick(()=>{
onFulfilled();
})
} else if (this.state === REJECTED) {
process.nextTick(()=>{
onRejected();
})
}
});
}
大致框架完成了,现在需要思考的问题有:
- 这个新Promise的决议是怎么决议的,显然上面的代码并没有对新Promise进行决议
- 这个新Promise的决议值怎么来
仔细阅读规范
- If either
onFulfilledoronRejectedreturns a valuex, run the Promise Resolution Procedure[[Resolve]](promise2, x).- If either
onFulfilledoronRejectedthrows an exceptione,promise2must be rejected witheas the reason.- If
onFulfilledis not a function andpromise1is fulfilled,promise2must be fulfilled with the same value aspromise1.- If
onRejectedis not a function andpromise1is rejected,promise2must be rejected with the same reason aspromise1
- 如果两个回调函数有返回值,则对新Promise进行一个「决议过程」
- 如果两个回调函数出错 error,新Promise执行Reject决议,并将 error 赋值给 reason
- 如果 onFulfilled 不是一个函数,并且原本promise被fulfilled,那么新promise 也用相同value对其进行fulfill
- 如果 onRejected 不是一个函数,并且原本的promise被 rejected,那么新promise也用相同reason对其进行reject
那么对于上面的两个问题,其实有了相应的答案:
- 新Promise决议时机:在执行完 onFulfilled 或者 onRejected之后决议
- 新Promise决议的值:取决于不同情况下的 onFulfilled 和 onRejected 的执行情况和返回值
目前,我们仅仅只是把两个回调函数加入了任务队列,并且在异步决议之后执行,我们如何对新Promise进行决议是一个需要解决的问题。这里必须要自己先思考一下解决方案!
其实解决办法很简单:将这两个回调函数做一层包装,在包装里面对新promise进行决议,将包装之后的函数加入任务队列中。
then:
then(onFulfilled, onRejected) {
onFulfilled = Utils.isFuntion(onFulfilled) ? onFulfilled.bind(undefined) : undefined;
onRejected = Utils.isFuntion(onRejected) ? onRejected.bind(undefined) : undefined;
return new MyPromise((res, rej) => {
// 对onFulfilled进行一层包装
const wrappedOnFulfilled = () => {
if (onFulfilled === undefined) {
// onFulfilled is not a function
res(this.value);
} else {
// onFulfilled is a function
const { value } = this;
try {
const x = onFulfilled(value);
res(x); // 这里的「决议过程」暂且用resolve方法代替,具体决议过程比较复杂,见下一节
} catch (error) {
rej(error);
}
}
};
// 对onRejected进行一层包装
const wrappedOnRejected = () => {
if (onRejected === undefined) {
rej(this.reason);
} else {
const { reason } = this;
try {
const x = onRejected(reason);
res(x);
} catch (error) {
rej(error);
}
}
};
if (this.state === PENDING) {
// 将包装之后的函数作为回调函数加入任务队列
this.onFulfilledTasks.push(wrappedOnFulfilled);
this.onRejectedTasks.push(wrappedOnRejected);
} else if (this.state === FULFILLED) {
process.nextTick(()=>{
wrappedOnFulfilled(); // 这里也要改对应的包装函数
})
} else if (this.state === REJECTED) {
process.nextTick(()=>{
wrappedOnRejected(); // 这里也要改对应的包装函数
})
}
});
}
至此,then方法(大致)就大功告成啦!
2.3 The Promise Resolution Procedure
这个所谓的决议过程可把我的脑汁给耗尽了,当我写完2.2以为我已经大功告成了,因为已经基本实现了Promise的功能,但是这个决议过程可谓是一个必不可少的步骤,而且这个规则的测试case也占了大半,是一个比较复杂的规则。
在2.2中,我们对新Promise的决议成功是直接调用resolve方法,但是这是不符合规则的,假如onFulfilled返回的是一个Promise,或者其他一个thenable对象,应当进行一个特殊处理,这就是这个决议过程存在的意义。对于具体含义参见下面这段英文描述。
The promise resolution procedure is an abstract operation taking as input a promise and a value, which we denote as [[Resolve]](promise, x). If x is a thenable, it attempts to make promise adopt the state of x, under the assumption that x behaves at least somewhat like a promise. Otherwise, it fulfills promise with the value x.
我的想法是:其实对于这个决议过程,是Promise类共有的一个方法,于是我选择将它定义在 MyPromise 类上,作为一个静态方法。
then(onFulfilled, onRejected) {
...
const p2 = new MyPromise((res, rej) => {
const wrappedOnFulfilled = () => {
if (onFulfilled === undefined) {
res(this.value);
} else {
try {
const x = onFulfilled(this.value);
MyPromise.resolutionProcedure(p2, x); // 修改
} catch (error) {
rej(error);
}
}
};
const wrappedOnRejected = () => {
if (onRejected === undefined) {
// 2.2.7.4
rej(this.reason);
} else {
try {
const x = onRejected(this.reason);
MyPromise.resolutionProcedure(p2, x); // 修改
} catch (error) {
rej(error);
}
}
};
...
});
return p2;
}
static resolutionProcedure(promise, x) {
// code here
}
下方的promise指的都是这个方法的第一个参数promise。
2.3.1
If promise and x refer to the same object, reject promise with a TypeError as the reason.
这条规则描述的就是 promise 和 x 不能是同一个对象,如果是直接对promise进行否决
if (promise === x) {
throw new TypeError('type error');
}
2.3.2
-
If
xis a promise, adopt its state :- If
xis pending,promisemust remain pending untilxis fulfilled or rejected. - If/when
xis fulfilled, fulfillpromisewith the same value. - If/when
xis rejected, rejectpromisewith the same reason.
- If
意思就是说,x如果是一个Promise类型,那么promise的决议跟随x,如果x被rejected,也用相同的reason来reject promise,fulfill同理。
if (Utils.isPromise(x)) {
x.then(
res => {
// 这里是一个很关键的点,我被卡了好久
// 之所以要递归调用决议过程,是因为这个res依旧有可能是一个Promise,或者是一个thenable的对象
// 因此不能直接对promise用res进行reslove
MyPromise.resolutionProcedure(promise, res);
},
err => {
promise.reject(err);
}
);
}
// 可以用下面这个测试case来理解为什么要递归调用
const yFactory = ()=> {
const outter = function (value) {
return {
then: function (onFulfilled) {
onFulfilled(value);
}
};
}
const inner = function (value) {
return {
then: function (onFulfilled) {
onFulfilled(value);
}
};
}
return new MyPromise(res=> res(outter(inner({sentinel: "sentinel"}))));
}
const xFactory = ()=> {
return {
then: function (resolvePromise) {
resolvePromise(yFactory());
}
}
}
function resolved(value) {
var d = deferred();
d.resolve(value);
return d.promise;
};
var promise = resolved({ dummy: "dummy" }).then(function onBasePromiseFulfilled() {
return xFactory();
});
promise.then(function onPromiseFulfilled(value) {
console.log('value: ', value);
});
2.3.3
Otherwise, if x is an object or function,
-
Let
thenbex.then. -
If retrieving the property
x.thenresults in a thrown exceptione, rejectpromisewitheas the reason. -
If
thenis a function, call it withxasthis, first argumentresolvePromise, and second argumentrejectPromise, where:-
If/when
resolvePromiseis called with a valuey, run[[Resolve]](promise, y). -
If/when
rejectPromiseis called with a reasonr, rejectpromisewithr. -
If both
resolvePromiseandrejectPromiseare called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored. -
If calling
thenthrows an exceptione,- If
resolvePromiseorrejectPromisehave been called, ignore it. - Otherwise, reject
promisewitheas the reason.
- If
-
-
If
thenis not a function, fulfillpromisewithx.
这个2.3.3 规则比较多,最基本的规则是x是一个对象或者一个函数。
2.3.3.1
用then来作为x.then的引用。(这点我起初不理解为什么单独搞一个引用,后来看测试case,猜测是因为测试需要)
2.3.3.2
如果获取 x.then 报错,那么直接用错误e来reject掉promsie。
if(){
...
} else if (Utils.isObject(x) || Utils.isFunction(x)) {
let xThen;
try {
xThen = x.then;
} catch (error) {
promise.reject(error);
return;
}
2.3.3.3
如果then是一个函数,用x作为this上下文来执行then,一个参数是resolvePromise,第二个参数是rejectPromise,并且,这两个参数永远只执行最先执行的那个,换句话说就是假如 resolvePromise 被call了多次,也只执行最先进来的那次,后续的直接忽略。如果then执行出错,如果已经执行过 resolvePromise 或者 rejectPromise,直接忽略,否则进行rejcet。
如果then不是一个函数,那么直接对promise用x进行resolve。
这里其实根据描述很好写出一下代码。
if (Utils.isFunction(xThen)) {
let hasCalledProcedure = false;
const reslovePromise = function (y) {
if (hasCalledProcedure) {
return;
}
hasCalledProcedure = true;
MyPromise.resolutionProcedure(promise, y);
};
const rejectPromise = function (r) {
if (hasCalledProcedure) {
return;
}
hasCalledProcedure = true;
promise.reject(r);
};
try {
xThen.call(x, reslovePromise, rejectPromise);
} catch (err) {
if (hasCalledProcedure) {
return;
}
hasCalledProcedure = true;
promise.reject(err);
}
} else {
promise.resolve(x);
}
2.3.3.4
如果 x 既不是对象,又不是函数,那么直接对promise用x进行resolve。
promise.resolve(x);
至此,一个符合Promise Aplus规范的MyPromise横空出世!
3. 代码总览
'use strict';
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
const Utils = {
canChangeState: state => state === PENDING,
isObject: val => typeof val === 'object' && val !== null,
isFunction: val => typeof val === 'function',
isPromise: val => val instanceof MyPromise,
};
class MyPromise {
state;
value;
reason;
onFulfilledTasks = [];
onRejectedTasks = [];
constructor(fn) {
this.state = PENDING;
Boolean(fn) && fn(this.resolve.bind(this), this.reject.bind(this));
}
resolve(value) {
if (Utils.canChangeState(this.state)) {
this.state = FULFILLED;
this.value = value;
process.nextTick(() => this.onFulfilledTasks.forEach(cb => cb(this.value)));
}
}
reject(reason) {
if (Utils.canChangeState(this.state)) {
this.state = REJECTED;
this.reason = reason;
process.nextTick(() => this.onRejectedTasks.forEach(cb => cb(this.reason)));
}
}
then(onFulfilled, onRejected) {
onFulfilled = Utils.isFunction(onFulfilled) ? onFulfilled.bind(undefined) : undefined;
onRejected = Utils.isFunction(onRejected) ? onRejected.bind(undefined) : undefined;
const p2 = new MyPromise((res, rej) => {
const wrappedOnFulfilled = () => {
if (onFulfilled === undefined) {
// 2.2.7.3
res(this.value);
} else {
// 2.2.7.1
try {
const x = onFulfilled(this.value);
MyPromise.resolutionProcedure(p2, x);
} catch (error) {
rej(error);
}
}
};
const wrappedOnRejected = () => {
if (onRejected === undefined) {
// 2.2.7.4
rej(this.reason);
} else {
try {
const x = onRejected(this.reason);
MyPromise.resolutionProcedure(p2, x);
} catch (error) {
rej(error);
}
}
};
if (this.state === PENDING) {
this.onFulfilledTasks.push(wrappedOnFulfilled);
this.onRejectedTasks.push(wrappedOnRejected);
} else if (this.state === FULFILLED) {
process.nextTick(() => {
wrappedOnFulfilled();
});
} else if (this.state === REJECTED) {
process.nextTick(() => {
wrappedOnRejected();
});
}
});
return p2;
}
static resolutionProcedure(promise, x) {
if (promise === x) {
throw new TypeError('type error');
}
if (Utils.isPromise(x)) {
x.then(
res => {
MyPromise.resolutionProcedure(promise, res);
},
err => {
promise.reject(err);
}
);
} else if (Utils.isObject(x) || Utils.isFunction(x)) {
let xThen;
try {
xThen = x.then;
} catch (error) {
promise.reject(error);
return;
}
if (Utils.isFunction(xThen)) {
let hasCalledProcedure = false;
const reslovePromise = function (y) {
if (hasCalledProcedure) {
return;
}
hasCalledProcedure = true;
MyPromise.resolutionProcedure(promise, y);
};
const rejectPromise = function (r) {
if (hasCalledProcedure) {
return;
}
hasCalledProcedure = true;
promise.reject(r);
};
try {
xThen.call(x, reslovePromise, rejectPromise);
} catch (err) {
if (hasCalledProcedure) {
return;
}
hasCalledProcedure = true;
promise.reject(err);
}
} else {
promise.resolve(x);
}
} else {
promise.resolve(x);
}
}
}
function deferred() {
let dfd = {};
dfd.promise = new MyPromise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}
module.exports = { deferred, MyPromise, Utils };
4. 测试结果
5. 总结
其实静下心来看规则一个个实现并不是很难,总代码量也就200行不到,最直接的收获其实就是对Promise有了更深一层次的了解。其实作为前端入门到进阶的一个小Lab是非常不错的。