只会用就out了,手写一个符合规范的Promise

Promise是什么

所谓Promise,简单说就是一个容器,里面保存着某个将来才会结束的事件(一般是一个异步操做)的结果。从语法上说,Promise 是一个对象,从它能够获取异步操做的消息。Promise 提供统一的 API,各类异步操做均可以用一样的方法进行处理。javascript

Promise是处理异步编码的一个解决方案,在Promise出现之前,异步代码的编写都是经过回调函数来处理的,回调函数自己没有任何问题,只是当屡次异步回调有逻辑关系时就会变得复杂:前端

const fs = require('fs');
fs.readFile('1.txt', (err,data) => {
    fs.readFile('2.txt', (err,data) => {
        fs.readFile('3.txt', (err,data) => {
            //可能还有后续代码
        });
    });
});
复制代码

上面读取了3个文件,它们是层层递进的关系,能够看到多个异步代码套在一块儿不是纵向发展的,而是横向,不管是从语法上仍是从排错上都很差,因而Promise的出现能够解决这一痛点。java

上述代码若是改写成Promise版是这样:面试

const util = require('util');
const fs = require('fs');
const readFile = util.promisify(fs.readFile);

readFile('1.txt')
    .then(data => {
        return readFile('2.txt');
    }).then(data => {
        return readFile('3.txt');
    }).then(data => {
        //...
    });
复制代码

能够看到,代码是从上至下纵向发展了,更加符合人们的逻辑。数组

下面手写一个Promise,按照Promises/A+规范,能够参照规范原文: Promises/A+规范promise

手写实现Promise是一道前端经典的面试题,好比美团的面试就是必考题,Promise的逻辑仍是比较复杂的,考虑的逻辑也比较多,下面总结手写Promise的关键点,和怎样使用代码来实现它。异步

Promise代码基本结构

实例化Promise对象时传入一个函数做为执行器,有两个参数(resolve和reject)分别将结果变为成功态和失败态。咱们能够写出基本结构函数

function Promise(executor) {
    this.state = 'pending'; //状态
    this.value = undefined; //成功结果
    this.reason = undefined; //失败缘由

    function resolve(value) {
        
    }

    function reject(reason) {

    }
}

module.exports = Promise;
复制代码

其中state属性保存了Promise对象的状态,规范中指明,一个Promise对象只有三种状态:等待态(pending)成功态(resolved)和失败态(rejected)。 当一个Promise对象执行成功了要有一个结果,它使用value属性保存;也有可能因为某种缘由失败了,这个失败缘由放在reason属性中保存。测试

then方法定义在原型上

每个Promise实例都有一个then方法,它用来处理异步返回的结果,它是定义在原型上的方法,咱们先写一个空方法作好准备:ui

Promise.prototype.then = function (onFulfilled, onRejected) {
};
复制代码

当实例化Promise时会当即执行

当咱们本身实例化一个Promise时,其执行器函数(executor)会当即执行,这是必定的:

let p = new Promise((resolve, reject) => {
    console.log('执行了');
});
复制代码

运行结果:

执行了
复制代码

所以,当实例化Promise时,构造函数中就要立刻调用传入的executor函数执行

function Promise(executor) {
    var _this = this;
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;

    executor(resolve, reject); //立刻执行
    
    function resolve(value) {}
    function reject(reason) {}
}
复制代码

已是成功态或是失败态不可再更新状态

规范中规定,当Promise对象已经由pending状态改变为了成功态(resolved)或是失败态(rejected)就不能再次更改状态了。所以咱们在更新状态时要判断,若是当前状态是pending(等待态)才可更新:

function resolve(value) {
        //当状态为pending时再作更新
        if (_this.state === 'pending') {
            _this.value = value;//保存成功结果
            _this.state = 'resolved';
        }

    }

    function reject(reason) {
    //当状态为pending时再作更新
        if (_this.state === 'pending') {
            _this.reason = reason;//保存失败缘由
            _this.state = 'rejected';
        }
    }
复制代码

以上能够看到,在resolve和reject函数中分别加入了判断,只有当前状态是pending才可进行操做,同时将成功的结果和失败的缘由都保存到对应的属性上。以后将state属性置为更新后的状态。

then方法的基本实现

当Promise的状态发生了改变,不管是成功或是失败都会调用then方法,因此,then方法的实现也很简单,根据state状态来调用不一样的回调函数便可:

Promise.prototype.then = function (onFulfilled, onRejected) {
    if (this.state === 'resolved') {
        //判断参数类型,是函数执行之
        if (typeof onFulfilled === 'function') {
            onFulfilled(this.value);
        }

    }
    if (this.state === 'rejected') {
        if (typeof onRejected === 'function') {
            onRejected(this.reason);
        }
    }
};
复制代码

须要一点注意,规范中说明了,onFulfilled 和 onRejected 都是可选参数,也就是说能够传也能够不传。传入的回调函数也不是一个函数类型,那怎么办?规范中说忽略它就行了。所以须要判断一下回调函数的类型,若是明确是个函数再执行它。

让Promise支持异步

代码写到这里彷佛基本功能都实现了,但是还有一个很大的问题,目前此Promise还不支持异步代码,若是Promise中封装的是异步操做,then方法无能为力:

let p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(1);
    },500);
});

p.then(data => console.log(data)); //没有任何结果
复制代码

运行以上代码发现没有任何结果,本意是等500毫秒后执行then方法,哪里有问题呢?缘由是setTimeout函数使得resolve是异步执行的,有延迟,当调用then方法的时候,此时此刻的状态仍是等待态(pending),所以then方法即没有调用onFulfilled也没有调用onRejected。

这个问题如何解决?咱们能够参照发布订阅模式,在执行then方法时若是还在等待态(pending),就把回调函数临时寄存到一个数组里,当状态发生改变时依次从数组中取出执行就行了,清楚这个思路咱们实现它,首先在类上新增两个Array类型的数组,用于存放回调函数:

function Promise(executor) {
    var _this = this;
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledFunc = [];//保存成功回调
    this.onRejectedFunc = [];//保存失败回调
    //其它代码略...
}
复制代码

这样当then方法执行时,若状态还在等待态(pending),将回调函数依次放入数组中:

Promise.prototype.then = function (onFulfilled, onRejected) {
    //等待态,此时异步代码尚未走完
    if (this.state === 'pending') {
        if (typeof onFulfilled === 'function') {
            this.onFulfilledFunc.push(onFulfilled);//保存回调
        }
        if (typeof onRejected === 'function') {
            this.onRejectedFunc.push(onRejected);//保存回调
        }
    }
    //其它代码略...
};
复制代码

寄存好了回调,接下来就是当状态改变时执行就行了:

function resolve(value) {
        if (_this.state === 'pending') {
            _this.value = value;
            //依次执行成功回调
            _this.onFulfilledFunc.forEach(fn => fn(value));
            _this.state = 'resolved';
        }

    }

    function reject(reason) {
        if (_this.state === 'pending') {
            _this.reason = reason;
            //依次执行失败回调
            _this.onRejectedFunc.forEach(fn => fn(reason));
            _this.state = 'rejected';
        }
    }
复制代码

至此,Promise已经支持了异步操做,setTimeout延迟后也可正确执行then方法返回结果。

链式调用

Promise处理异步代码最强大的地方就是支持链式调用,这块也是最复杂的,咱们先梳理一下规范中是怎么定义的:

  1. 每一个then方法都返回一个新的Promise对象(原理的核心
  2. 若是then方法中显示地返回了一个Promise对象就以此对象为准,返回它的结果
  3. 若是then方法中返回的是一个普通值(如Number、String等)就使用此值包装成一个新的Promise对象返回。
  4. 若是then方法中没有return语句,就视为返回一个用Undefined包装的Promise对象
  5. 若then方法中出现异常,则调用失败态方法(reject)跳转到下一个then的onRejected
  6. 若是then方法没有传入任何回调,则继续向下传递(值的传递特性)。

规范中说的很抽像,咱们能够把很差理解的点使用代码演示一下。

其中第3项,若是返回是个普通值就使用它包装成Promise,咱们用代码来演示:

let p =new Promise((resolve,reject)=>{
    resolve(1);
});

p.then(data=>{
    return 2; //返回一个普通值
}).then(data=>{
    console.log(data); //输出2
});
复制代码

可见,当then返回了一个普通的值时,下一个then的成功态回调中便可取到上一个then的返回结果,说明了上一个then正是使用2来包装成的Promise,这符合规范中说的。

第4项,若是then方法中没有return语句,就视为返回一个用Undefined包装的Promise对象

let p = new Promise((resolve, reject) => {
    resolve(1);
});

p.then(data => {
    //没有return语句
}).then(data => {
    console.log(data); //undefined
});
复制代码

能够看到,当没有返回任何值时不会报错,没有任何语句时实际上就是return undefined;即将undefined包装成Promise对象传给下一个then的成功态。

第6项,若是then方法没有传入任何回调,则继续向下传递,这是什么意思呢?这就是Promise中值的穿透,仍是用代码演示一下:

let p = new Promise((resolve, reject) => {
    resolve(1);
});

p.then(data => 2)
.then()
.then()
.then(data => {
    console.log(data); //2
});
复制代码

以上代码,在第一个then方法以后连续调用了两个空的then方法 ,没有传入任何回调函数,也没有返回值,此时Promise会将值一直向下传递,直到你接收处理它,这就是所谓的值的穿透。

如今能够明白链式调用的原理,不管是何种状况then方法都会返回一个Promise对象,这样才会有下个then方法。

搞清楚了这些点,咱们就能够动手实现then方法的链式调用,一块儿来完善它:

Promise.prototype.then = function (onFulfilled, onRejected) {
    var promise2 = new Promise((resolve, reject) => {
    //代码略...
    }
    return promise2;
};
复制代码

首先,不论何种状况then都返回Promise对象,咱们就实例化一个新promise2并返回。

接下来就处理根据上一个then方法的返回值来生成新Promise对象,因为这块逻辑较复杂且有不少处调用,咱们抽离出一个方法来操做,这也是规范中说明的:

/** * 解析then返回值与新Promise对象 * @param {Object} promise2 新的Promise对象 * @param {*} x 上一个then的返回值 * @param {Function} resolve promise2的resolve * @param {Function} reject promise2的reject */
function resolvePromise(promise2, x, resolve, reject) {
    //...
}
复制代码

resolvePromise方法用来封装链式调用产生的结果,下面咱们分别一个个状况的写出它的逻辑,首先规范中说明,若是promise2x 指向同一对象,就使用TypeError做为缘由转为失败。原文以下:

If promise and x refer to the same object, reject promise with a TypeError as the reason.

这是什么意思?其实就是循环引用,当then的返回值与新生成的Promise对象为同一个(引用地址相同),则会抛出TypeError错误:

let promise2 = p.then(data => {
    return promise2;
});
复制代码

运行结果:

TypeError: Chaining cycle detected for promise #<Promise>
复制代码

很显然,若是返回了本身的Promise对象,状态永远为等待态(pending),再也没法成为resolved或是rejected,程序会死掉,所以首先要处理它:

function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        reject(new TypeError('Promise发生了循环引用'));
    }
}
复制代码

接下来就是分各类状况处理。当x就是一个Promise,那么就执行它,成功即成功,失败即失败。若x是一个对象或是函数,再进一步处理它,不然就是一个普通值:

function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        reject(new TypeError('Promise发生了循环引用'));
    }

    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        //多是个对象或是函数
    } else {
        //不然是个普通值
        resolve(x);
    }
}
复制代码

此时规范中说明,如果个对象,则尝试将对象上的then方法取出来,此时若是报错,那就将promise2转为失败态。原文:

If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.

function resolvePromise(promise2, x, resolve, reject) {
    //代码略...
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        //多是个对象或是函数
        try {
            let then = x.then;//取出then方法引用
        } catch (e) {
            reject(e);
        }
        
    } else {
        //不然是个普通值
        resolve(x);
    }
}
复制代码

多说几句,为何取对象上的属性有报错的可能?Promise有不少实现(bluebird,Q等),Promises/A+只是一个规范,你们都按此规范来实现Promise才有可能通用,所以全部出错的可能都要考虑到,假设另外一我的实现的Promise对象使用Object.defineProperty()恶意的在取值时抛错,咱们能够防止代码出现Bug。

此时,若是对象中有then,且then是函数类型,就能够认为是一个Promise对象,以后,使用x做为this来调用then方法。

If then is a function, call it with x as this

//其余代码略...
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    //多是个对象或是函数
    try {
        let then = x.then; 
        if (typeof then === 'function') {
            //then是function,那么执行Promise
            then.call(x, (y) => {
                resolve(y);
            }, (r) => {
                reject(r);
            });
        } else {
            resolve(x);
        }
    } catch (e) {
        reject(e);
    }

} else {
    //不然是个普通值
    resolve(x);
}
复制代码

这样链式写法就基本完成了。可是还有一种极端的状况,若是Promise对象转为成功态或是失败时传入的仍是一个Promise对象,此时应该继续执行,直到最后的Promise执行完。

p.then(data => {
    return new Promise((resolve,reject)=>{
        //resolve传入的仍是Promise
        resolve(new Promise((resolve,reject)=>{
            resolve(2);
        }));
    });
})
复制代码

此时就要使用递归操做了。

规范中原文以下:

If a promise is resolved with a thenable that participates in a circular thenable chain, such that the recursive nature of [[Resolve]](promise, thenable) eventually causes [[Resolve]](promise, thenable) to be called again, following the above algorithm will lead to infinite recursion. Implementations are encouraged, but not required, to detect such recursion and reject promise with an informative TypeError as the reason.

很简单,把调用resolve改写成递归执行resolvePromise方法便可,这样直到解析Promise成一个普通值才会终止,即完成此规范:

//其余代码略...
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    //多是个对象或是函数
    try {
        let then = x.then; 
        if (typeof then === 'function') {
            let y = then.call(x, (y) => {
                //递归调用,传入y如果Promise对象,继续循环
                resolvePromise(promise2, y, resolve, reject);
            }, (r) => {
                reject(r);
            });
        } else {
            resolve(x);
        }
    } catch (e) {
        reject(e);
    }

} else {
    //是个普通值,最终结束递归
    resolve(x);
}

复制代码

到此,链式调用的代码已所有完毕。在相应的地方调用resolvePromise方法便可。

最后的最后

其实,写到此处Promise的真正源码已经写完了,可是距离100分还差一分,是什么呢?

规范中说明,Promise的then方法是异步执行的。

onFulfilled or onRejected must not be called until the execution context stack contains only platform code.

ES6的原生Promise对象已经实现了这一点,可是咱们本身的代码是同步执行,不相信能够试一下,那么如何将同步代码变成异步执行呢?可使用setTimeout函数来模拟一下:

setTimeout(()=>{
    //此处的代码会异步执行
},0);
复制代码

利用此技巧,将代码then执行处的全部地方使用setTimeout变为异步便可,举个栗子:

setTimeout(() => {
    try {
        let x = onFulfilled(value);
        resolvePromise(promise2, x, resolve, reject);
    } catch (e) {
        reject(e);
    }
},0);
复制代码

好了,如今已是满分的Promise源码了。

满分的测试

好不容易写好的Promise源码,最终是否真的符合Promises/A+规范,开源社区提供了一个包用于测试咱们的代码:promises-aplus-tests

这个包的使用方法不在详述,此包能够一项项的检查咱们写的代码是否合规,若是有任一项不符就会给咱们报出来,若是检查你的代码一路都是绿色,那恭喜,你的Proimse已经合法了,能够上线提供给别人使用了:

872项测试经过!

如今源码都会写,终于能够自信的回答面试官的问题了。

相关文章
相关标签/搜索