手写一个符合 Promise A+ 规范的 Promise
0. 相关文档
Promise A+规范
promises-aplus-tests
1. 准备工作
安装 promises-aplus-tests
,编写测试代码。
npm initnpm install promises-aplus-tests -D
// src/index.jsconst promisesAplusTests = require("promises-aplus-tests");const adapter = require("./MyPromise") // 这里就是要自己写的PromisepromisesAplusTests(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 with value
.
rejected(reason)
: creates a promise that is already rejected with reason
.
deferred(): creates an object consisting of { promise, resolve, reject }:
promise
is a promise that is currently in the pending state.
resolve(value)
resolves the promise with value
.
reject(reason)
moves the promise from the pending state to the rejected state, with rejection reason reason
.
The resolved
and rejected
exports are actually optional, and will be automatically created by the test runner using deferred
if 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 then
method
A promise’s then
method accepts two arguments:
promise.then(onFulfilled, onRejected)
Both onFulfilled and onRejected are optional arguments:
If onFulfilled
is not a function, it must be ignored.
If onRejected
is not a function, it must be ignored.
If onFulfilled is a function:
it must be called after promise
is fulfilled, with promise
’s value as its first argument.
it must not be called before promise
is fulfilled.
it must not be called more than once.
If onRejected is a function,
it must be called after promise
is rejected, with promise
’s reason as its first argument.
it must not be called before promise
is rejected.
it must not be called more than once.
onFulfilled
or onRejected
must not be called until the execution context stack contains only platform code. [3.1].
onFulfilled
and onRejected
must be called as functions (i.e. with no this
value). [3.2]
then may be called multiple times on the same promise.
If/when promise
is fulfilled, all respective onFulfilled
callbacks must execute in the order of their originating calls to then
.
If/when promise
is rejected, all respective onRejected
callbacks must execute in the order of their originating calls to then
.
then
must return a promise [3.3].
promise2 = promise1.then(onFulfilled, onRejected);
If either onFulfilled
or onRejected
returns a value x
, run the Promise Resolution Procedure [[Resolve]](promise2, x)
.
If either onFulfilled
or onRejected
throws an exception e
, promise2
must be rejected with e
as the reason.
If onFulfilled
is not a function and promise1
is fulfilled, promise2
must be fulfilled with the same value as promise1
.
If onRejected
is not a function and promise1
is rejected, promise2
must be rejected with the same reason as promise1
.
这个 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之后调用,且携带相应参数
// src/index.jsconst promisesAplusTests = require("promises-aplus-tests");const adapter = require("./MyPromise") // 这里就是要自己写的PromisepromisesAplusTests(adapter, function (err) { // All done; output is in the console. Or check `err` for number of failures.});0
2.2.3
onRejected必须在rejected之后调用,且携带相应参数
// src/index.jsconst promisesAplusTests = require("promises-aplus-tests");const adapter = require("./MyPromise") // 这里就是要自己写的PromisepromisesAplusTests(adapter, function (err) { // All done; output is in the console. Or check `err` for number of failures.});1
2.2.4
来说就是 onFulfilled 和 onRejected 必须异步调用
这里我的想法是在循环遍历回调队列的时候,异步执行每一个回调函数,用代码表述就是这样:
// src/index.jsconst promisesAplusTests = require("promises-aplus-tests");const adapter = require("./MyPromise") // 这里就是要自己写的PromisepromisesAplusTests(adapter, function (err) { // All done; output is in the console. Or check `err` for number of failures.});2
2.2.5
onFulfilled 和 onRejected被调用时无this,这里必须绑定到undefined,不能绑定到null,不然测试case会报错
// src/index.jsconst promisesAplusTests = require("promises-aplus-tests");const adapter = require("./MyPromise") // 这里就是要自己写的PromisepromisesAplusTests(adapter, function (err) { // All done; output is in the console. Or check `err` for number of failures.});3
2.2.6
同一个Promise,多个then注册必须按顺序执行多次回调
这里有个测试的case需要注意:
// src/index.jsconst promisesAplusTests = require("promises-aplus-tests");const adapter = require("./MyPromise") // 这里就是要自己写的PromisepromisesAplusTests(adapter, function (err) { // All done; output is in the console. Or check `err` for number of failures.});4
一开始没有考虑到这种case的时候的代码:
// src/index.jsconst promisesAplusTests = require("promises-aplus-tests");const adapter = require("./MyPromise") // 这里就是要自己写的PromisepromisesAplusTests(adapter, function (err) { // All done; output is in the console. Or check `err` for number of failures.});5
出现的问题:
// src/index.jsconst promisesAplusTests = require("promises-aplus-tests");const adapter = require("./MyPromise") // 这里就是要自己写的PromisepromisesAplusTests(adapter, function (err) { // All done; output is in the console. Or check `err` for number of failures.});6
改进:
// src/index.jsconst promisesAplusTests = require("promises-aplus-tests");const adapter = require("./MyPromise") // 这里就是要自己写的PromisepromisesAplusTests(adapter, function (err) { // All done; output is in the console. Or check `err` for number of failures.});7
2.2.7
then必须返回一个promise
这个要求其实是then方法中最难理解的一个规则,我也是琢磨了好半天才悟出其中真谛。
首先返回一个Promise,那我们先把大致框架给写好,再往里面填东西。
// src/index.jsconst promisesAplusTests = require("promises-aplus-tests");const adapter = require("./MyPromise") // 这里就是要自己写的PromisepromisesAplusTests(adapter, function (err) { // All done; output is in the console. Or check `err` for number of failures.});8
大致框架完成了,现在需要思考的问题有:
这个新Promise的决议是怎么决议的,显然上面的代码并没有对新Promise进行决议
这个新Promise的决议值怎么来
仔细阅读规范
If either onFulfilled
or onRejected
returns a value x
, run the Promise Resolution Procedure [[Resolve]](promise2, x)
.
If either onFulfilled
or onRejected
throws an exception e
, promise2
must be rejected with e
as the reason.
If onFulfilled
is not a function and promise1
is fulfilled, promise2
must be fulfilled with the same value as promise1
.
If onRejected
is not a function and promise1
is rejected, promise2
must be rejected with the same reason as promise1
如果两个回调函数有返回值,则对新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:
// src/index.jsconst promisesAplusTests = require("promises-aplus-tests");const adapter = require("./MyPromise") // 这里就是要自己写的PromisepromisesAplusTests(adapter, function (err) { // All done; output is in the console. Or check `err` for number of failures.});9
至此,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 类上,作为一个静态方法。
deferred(): creates an object consisting of { promise, resolve, reject }:0
下方的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进行否决
deferred(): creates an object consisting of { promise, resolve, reject }:1
2.3.2
If x
is a promise, adopt its state :
If x
is pending, promise
must remain pending until x
is fulfilled or rejected.
If/when x
is fulfilled, fulfill promise
with the same value.
If/when x
is rejected, reject promise
with the same reason.
意思就是说,x如果是一个Promise类型,那么promise的决议跟随x,如果x被rejected,也用相同的reason来reject promise,fulfill同理。
deferred(): creates an object consisting of { promise, resolve, reject }:2
2.3.3
Otherwise, if x
is an object or function,
Let then
be x.then
.
If retrieving the property x.then
results in a thrown exception e
, reject promise
with e
as the reason.
If then
is a function, call it with x
as this
, first argument resolvePromise
, and second argument rejectPromise
, where:
If resolvePromise
or rejectPromise
have been called, ignore it.
Otherwise, reject promise
with e
as the reason.
If/when resolvePromise
is called with a value y
, run [[Resolve]](promise, y)
.
If/when rejectPromise
is called with a reason r
, reject promise
with r
.
If both resolvePromise
and rejectPromise
are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.
If calling then
throws an exception e
,
If then
is not a function, fulfill promise
with x
.
这个2.3.3 规则比较多,最基本的规则是x是一个对象或者一个函数。
2.3.3.1
用then来作为x.then的引用。(这点我起初不理解为什么单独搞一个引用,后来看测试case,猜测是因为测试需要)
2.3.3.2
如果获取 x.then 报错,那么直接用错误e来reject掉promsie。
deferred(): creates an object consisting of { promise, resolve, reject }:3
2.3.3.3
如果then是一个函数,用x作为this上下文来执行then,一个参数是resolvePromise
,第二个参数是rejectPromise
,并且,这两个参数永远只执行最先执行的那个,换句话说就是假如 resolvePromise 被call了多次,也只执行最先进来的那次,后续的直接忽略。如果then
执行出错,如果已经执行过 resolvePromise 或者 rejectPromise,直接忽略,否则进行rejcet。
如果then不是一个函数,那么直接对promise用x进行resolve。
这里其实根据描述很好写出一下代码。
deferred(): creates an object consisting of { promise, resolve, reject }:4
2.3.3.4
如果 x 既不是对象,又不是函数,那么直接对promise用x进行resolve。
deferred(): creates an object consisting of { promise, resolve, reject }:5
至此,一个符合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 { deferred(): creates an object consisting of { promise, resolve, reject }:5 } } else { deferred(): creates an object consisting of { promise, resolve, reject }:5 } }}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是非常不错的。
原文:https://juejin.cn/post/7097590762140860430