手写一个符合A+规范的Promise
本文同时也发布在我的github博客上,欢迎star~
之前也手写过简单的promise,这次则是为了通过官方的Promise A+测试集,借鉴了一些下载量较多的promise polyfill,改了几遍,终于是通过了A+规范的872个测试用例
如何测试?
测试库地址在这:promises-tests ,大家在写完自己的promise后,不妨也去测试一下,检验自己的promise是否符合Promise A+规范。这个库使用起来很方便,像下面这样就可以了:
const tests = require("promises-aplus-tests"); const Promise = require("./index"); const deferred = function() { let resolve, reject; const promise = new Promise(function(_resolve, _reject) { resolve = _resolve; reject = _reject; }); return { promise: promise, resolve: resolve, reject: reject }; }; const adapter = { deferred }; tests.mocha(adapter);
其中,index.js中是你写的Promise
实现
首先我们定义一些全局属性:
const IS_ERROR = {}; let ERROR = null;
IS_ERROR
作为发生错误时的标识,ERROR
用来保存错误;
做好准备工作,再来定义_Promise类,其中fn是Promise接受的函数,构造函数执行时立刻调用;_status是Promise的状态,初始为0(pending),resolved时为1,rejected时为2;_value用来保存Promise resolved时的返回值和rejected时的失败信息;_handlers用来保存Promise成功和失败时调用的处理方法
function _Promise(fn) { this._status = 0; this._value = null; this._handlers = []; doFn(this, fn); }
最后执行doFn方法,传入this值和fn:
function doFn(self, fn) { const ret = safeCallTwo( fn, function(value) { self.resolve(value); }, function(reason) { self.reject(reason); } ); if (ret === IS_ERROR) { self.reject(ERROR); } }
其中safeCallTwo
是用来安全执行两参数方法的函数,当执行出错时,捕获错误,保存在ERROR
中,返回IS_ERROR
标识:
function safeCallTwo(fn, arg1, arg2) { try { return fn(arg1, arg2); } catch (error) { ERROR = error; return IS_ERROR; } }
在doFn
中,调用safeCallTwo
,fn传入两个参数供我们调用,也就是我们常用的resolve
方法和reject
方法,并获取到返回值,如果ret为错误标识IS_ERROR
,则调用reject
_Promise原型上挂载着resolve和reject方法,如下:
_Promise.prototype.resolve = function(value) { if (this._status !== 0) { return; } this._status = 1; this._value = value; doThen(this); }; _Promise.prototype.reject = function(reason) { if (this._status !== 0) { return; } this._status = 2; this._value = reason; doThen(this); };
因为Promise的状态只能由pending
转为resolved
和rejected
,所以在执行resolve和reject方法时,要先判断status是否为0,若不为0,直接return;修改status和value后,执行doThen方法:
function doThen(self) { const handlers = self._handlers; handlers.forEach(handler => { doHandler(self, handler); }); }
doThen函数的作用是从self上取出的handlers并依次执行
我们再来看一看挂载在原型上的then方法:
_Promise.prototype.then = function(onResolve, onReject) { const res = new _Promise(function() {}); preThen(this, onResolve, onReject, res); return res; };
我们知道,Promise是支持链式调用的,所以我们的then方法也会返回一个Promise,以供后续调用;
下面是preThen方法:
function preThen(self, onResolve, onReject, res) { onResolve = typeof onResolve === "function" ? onResolve : null; onReject = typeof onReject === "function" ? onReject : null; const handler = { onResolve, onReject, promise: res }; if (self._status === 0) { self._handlers.push(handler); return; } doHandler(self, handler); }
preThen方法接受4个值,分别为当前Promise——self,resolve后的回调函数onResolve,reject后的回调函数onReject,then函数返回的promise——res。先判断onResolve和onReject是否为函数,若不是,直接置为null。再将onResolve、onReject、res放入handler对象中
接下来需要注意,Promise接受的函数(也就是上文的fn)并不是一定是异步调用resolve和reject,也有可能是同步的,也就是说在执行preThen函数时,self的status可能已经不为0了,这时候我们就不需要将handler保存起来等待调用,而是直接调用回调函数
doHandler函数代码见下:
function doHandler(self, handler) { setTimeout(() => { const { onReject, onResolve, promise } = handler; const { _status, _value } = self; const handlerFun = _status === 1 ? onResolve : onReject; if (handlerFun === null) { _status === 1 ? promise.resolve(_value) : promise.reject(_value); return; } const ret = safeCallOne(handlerFun, _value); if (ret === IS_ERROR) { promise.reject(ERROR); return; } promise.resolve(ret); }); }
我们知道,即使是同步执行relove或者reject,then函数接受的回调函数也不会立刻同步执行,如下代码会依次输出1,3,2,而非1,2,3
const p = new Promise(resolve => { console.log(1); resolve(); }); p.then(() => { console.log(2); }); console.log(3);
在这里,我使用了setTimeout来模拟这种模式,当然,这只是一种粗糙的模拟,更好的方式是引入或实现类似asap的库(下个星期我可能会实现这个,哈哈),但setTimeout也足够通过测试了
doHandler函数中,我们调用相应的回调函数,需要注意的是,如果相应回调函数为null(null是前文判断回调函数不为function时统一赋值的),则直接调用then函数返回的promise的resolve或reject方法。
同样,我们使用了safeCallOne来捕获错误,这里不再赘述
到这里,我们执行测试,发现不出意外地没有通过,因为我们只是实现了基础的Promise,还没有实现resolve中的thenable功能,下面是mdn对于thenable的描述:
返回一个状态由给定value决定的Promise对象。如果该值是thenable(即,带有then方法的对象),返回的Promise对象的最终状态由then方法执行决定;否则的话(该value为空,基本类型或者不带then方法的对象),返回的Promise对象状态为fulfilled,并且将该value传递给对应的then方法。通常而言,如果你不知道一个值是否是Promise对象,使用Promise.resolve(value) 来返回一个Promise对象,这样就能将该value以Promise对象形式使用
我们再来修改resolve方法:
_Promise.prototype.resolve = function(value) { if (this._status !== 0) { return; } if (this === value) { return this.reject(new TypeError("cant's resolve itself")); } if (value && (typeof value === "function" || typeof value === "object")) { const then = getThen(value); if (then === IS_ERROR) { this.reject(ERROR); return; } if (value instanceof _Promise) { value.then( value => { this.resolve(value); }, reason => { this.reject(reason); } ); return; } if (typeof then === "function") { doFn(this, then.bind(value)); return; } } this._status = 1; this._value = value; doThen(this); };
先判断this和value是否为一个Promise,若是一个,则抛出错误
再判断value的类型是否为function或object,如果是,则实行getThen方法进行错误捕获:
function getThen(self) { try { return self.then; } catch (error) { ERROR = error; return IS_ERROR; } }
若成功拿到then方法,检测value instanceof _Promise
,若为true,则直接采用value的状态和value或者reason。
若then为function,则将then函数以value为this值,当作fn执行,也就是达成下面代码的效果:
const p = new Promise(resolve => { resolve({ then: _resolve => { _resolve(1); } }); }); p.then(value => console.log(value)); //打印1
我们再次执行测试,发现仍然有错,其因出现在下面这种情况下:
const p = new _Promise(resolve => { resolve({ then: _resolve => { setTimeout(() => _resolve(1)), 500; } }); resolve(2); }); p.then(value => console.log(value));
这个时候,使用我们的Promise,输出的是2,而在规范中,应当是输出1
原因是我们在对象的then方法中是异步地resolve,这个时候,下面的resolve(2)
在执行时,status还没有变,自然可以修改status和value
解决方法也很简单,只用在doFn方法中判断是否为第一次执行即可:
function doFn(self, fn) { let done = false; const ret = safeCallTwo( fn, function(value) { if (done) { return; } done = true; self.resolve(value); }, function(reason) { if (done) { return; } done = true; self.reject(reason); } ); if (ret === IS_ERROR) { if (done) { return; } done = true; self.reject(ERROR); } }
再执行测试,发现已经测试用例全部通过~
代码
完整代码已放在我的github上,地址为https://github.com/Bowen7/playground/tree/master/promise-polyfill ,可以clone我的playground项目,再到promise-polyfill目录下npm install
,然后执行npm test
即可运行测试