用最少的代码手工实现一个Promise,5分钟看懂

Promise简介

Promise 采用面向对象的方式封装了回调函数,可以将回调金字塔改为平行的链式写法,优雅的解决了回调地狱,ES7带来了异步的终级解决方案async/await,可以用写同步代码的方式编写异步代码,而Promise正是async/await的基石。

Promise 是一种设计模式,也是规范,历史上曾经出现过Promise A/Promise A+/Promise B/Promise D四种规范,最终ES6选择了Promise A+的方案,真理来之不易。

Promise表面上看起来比较简单,你看,生成的Promise对象很纯净,只有then,catch,finally几个方法,还有两个隐藏的属性:PromiseStatus和PromiseValue分别表示状态和返回值

说到Promise状态,众所周知,只有pending,fulfilled,rejected 3种状态,而且不可逆,不成功便成仁,从创建实例时的pending,调用成功resolve就变成fulfilled,失败则变为rejected,整个模型非常简单

但是深入了解,Promise还有许多的潜规则,要深入理解一项技术最好的办法是造一个轮子。

Promise能够一统江湖成为异步的终极解决方案(配合async/await),它的价值绝对是不可估量的,值得你亲手实现不止一遍。

极简版Promise

需求分析:

万丈高楼平地起,一砖一瓦靠自己。我们先从最简单的核心功能开始,第一步仅实现Promise的构造器函数和then方法 两个功能

功能清单:

  1. Promise 构造器函数,传入一个函数,该函数立即执行,并且有resolve和reject两个参数,resolve被调用时Promise状态变为fulfilled
  2. 实现then方法,传入一个函数,该该数在Promise被fulfilled时执行

代码实现:

class PromiseA {
  constructor(init) {
    this.PromiseStatus = 'pending';
    var resolve=(val)=>{
        if(this.resolveCallback){
            this.PromiseStatus="fulfilled"
            this.resolveCallback(val);
        }
    }
    if(init){
        init(resolve,reject);
    }
  }
  then(onFulfill,onReject) {
    this.resolveCallback=onFulfill;
    this.rejectCallback=onReject;
    return this;
  }
}
复制代码

就这么简单,花几分钟就可以写好,写个测试代码跑一下

new PromiseA(function (resolve){
    setTimeout(function (){
        resolve("hello,from promise 1");
    },2000)
}).then(function (msg){
    console.log(msg);
})
复制代码

两秒后输出了:hello,from promise 1

完美运行,能够转得动的轮子就是好轮子!

但是,好像还缺了点什么?毕竟我们想做的是奔驰车的轮子......

完整版Promise

需求分析

上一步我们做出了第一个能运行起来的Promise,但是还缺失一些必备功能,如下:

    1. 每次调用then方法应该返回一个新的Promise对象
    1. then方法支持链式调用,链式调用有两种用法:
  •    2.1 then注册的onFulfill函数没有返回值,则之后的then全部依次触发
    复制代码
  •    2.2 then注册的onFulfill函数返回了新的promise,则等这个新的promise fulfill之后,再触发之后的then
    复制代码
    1. then 方法需要支持 调用多次
    1. reject函数,以及catch方法

实现思路:

精简版的Promise 很容易实现和读懂,但是链式调用要怎么实现是个难题。

链式调用的两种用法,一种是在onFulfill中不返回promise对象的,自动生成一个promise 对象,并在onfulfill函数return 后自动resolve,这样就会直接触发下一级的then,后面的onfulfill同理,会一直往后触发下去,这样就实现了同一时间依次调用后续的所有then

另一种是在在onFulfill中返回promise对象的,会替换自动生成的promise对象,新的promise对象resolve后, 触发下一个then

两种用法的本质其实是一样的,区别就在于then中新生成的promise会不会自动reslove

链式调用搞清楚了,但是实现起来就有点烧脑了,因为链式调用本身是链表的数据结构,又是高阶函数传来传去,很容易绕晕,我是花了很久时间调试修改,实现思路也是在调试过程中才慢慢理清的,虽然只有几行代码,但是用语言描述比较晦涩难懂,你非得单步调试一下才能明白其中的奥妙。主要实现思路如下:

  1. 首先,在then方法中返回一个新的promise不是什么难事,new一下就可以了,但是then方法如果返回了promise,要用新的promise替代,问题是then中的promise已经先返回了,这是先有鸡还是先有蛋的问题,时光不能倒流,那只有通过引用传递,改写之前返回的promise了,其实也不用完全替换,只需要改写原有promise的resolve,reject 就可以了。

  2. 对于then方法注册的回调函数中不返回promise的情况 ,因为每个then都返回了新的promise,当前的promise 判定没有手工返回新的promise,如果有就自动resolve,并将其返回值做为value值传入resolve,这样就会像多米诺骨牌一样触发下一级then, 如果没有return 值,则相当于 resolve(undefined)。

  3. then 方法可以被同一个 promise 调用多次 当 promise 成功执行时,所有 onFulfilled 需按照其注册顺序依次回调 当 promise 被拒绝执行时,所有的 onRejected 需按照其注册顺序依次回调

最开始写的版本用的普通变量保存resolve回调,已经修改为数组,这样才能支持多次调用then

完整代码实现:

class PromiseA {
 constructor(init) {
        this.PromiseStatus = 'pending';
        this.PromiseValue = null;
        this.reason = null;
        this.resolveCallback = [];
        this.rejectCallback = [];
        var resolve = (val) => {
            setTimeout(()=>{
                if(this.PromiseStatus != "pending"){return ;}
                this.PromiseValue = val;
                this.PromiseStatus = "fulfilled"
                if (this.resolveCallback) {
                    this.resolveCallback.forEach( (resolveFn) => {
                        if(resolveFn === val) { 
                            return reject(new TypeError('promise and x refer to the same object'));
                        }
                        var resolveResult=resolveFn(val);
                        let next = this.nextPromise;
                        if (resolveResult instanceof PromiseA) { //then方法返回了新的promise,

                            resolveResult.resolveCallback = next.resolveCallback;
                            resolveResult.rejectCallback = next.rejectCallback;
                        } else { //未返回新的promise,直接用返回值resolve
                            if (next != undefined && next.resolveCallback && next.resolveCallback != this.resolveCallback) { //非空检测,防重复调用
                                next.resolve(resolveResult);
                            }
                        }
                    })
                }
            })
        }
        var reject = (reason) => {
            setTimeout(()=>{
                if(this.PromiseStatus != "pending"){return ;}
                if (this.rejectCallback) {
                    this.PromiseStatus = "rejected"
                    this.reason=reason;
                    this.rejectCallback.forEach( (rejectFn) => {
                        rejectFn(reason)
                    });
                }
            })
        }
        this.resolve=resolve;
        this.reject=reject;
     
        if (init) {
            init(resolve, reject);
        }
    }
    then(onFulfill, onReject) {
        if(typeof onFulfill!="function"){
            onFulfill=function(value) {return value;};
        }
        if(typeof onReject!="function"){
            onReject=function(err) {throw err;}
        }
        this.resolveCallback.push(onFulfill);
        this.rejectCallback.push(onReject);
        var promise = new PromiseA();//创建一个新的promise实例
        this.nextPromise = promise;//保存一下新的promise引用,便于链式调用
        if (this.PromiseStatus == "fulfilled") { //如果是已经fulfilled的promise,立即执行
            this.resolve(this.PromiseValue);
        } else if (this.PromiseStatus == "rejected"){
            this.reject(this.reason);
        }

        return promise;
    }
    catch(onRejected) {
        return this.then(null, onRejected);
    }
}
复制代码

写个测试用例跑一下:


console.time("timer1");  
console.time("timer2");  

new PromiseA(function (resolve){
    setTimeout(function (){
        resolve("hello,from promise 1");
        
    },2000)
}).then(function (msg){
    console.log(msg);
    console.timeEnd("timer1");

}).then(function (msg){
    console.log(msg)
    console.timeEnd("timer2");
})
复制代码

运行后,两个then回调在2秒后同时触发,说明第一种链式调用验证成功

再测试一下第二种链式调用,测试代码如下:

console.time("timer1");  
console.time("timer2");  

new PromiseA(function (resolve){
    setTimeout(function (){
        resolve("hello,from promise 1");
        
    },2000)
}).then(function (msg){
    console.log(msg);
    console.timeEnd("timer1");
    return new PromiseA(function (resolve){
        setTimeout(function (){
            resolve("world,from promise 2")
        },3000)
    })
}).then(function (msg){
    console.log(msg)
    console.timeEnd("timer2");
})
复制代码

验证成功,在2秒后触发了第一个then回调,并接收到了hello,from promise1的返回值,在5秒后触发了第二个then回调,并接收到了"world,from promise2"的返回值

感谢kscript提出的问题,即不注册then和resolve之后再注册then产生的问题,目前最近的代码已经修正了。

实现Promise.all和Promise.race

Promise的实例功能已经完工了,翻翻看Promise构造器函数上还有两个类方法all和race,其中Promise.all是一个非常有用的功能,可以并发执行多个异步任务,全部成功后再执行resolve,无论是处理多个http并行请求,还是并行执行sql脚本等并行计算任务,都十分方便。

有了上面的PromiseA类基础设施,实现这个功能简直不要太简单。这次要用静态方法,也叫类方法,就是在PromiseA构造器函数上定义的,es6 的class 中定义的方法默认是生成在实例的原型中的,加一个static关键字就可以变为静态方法。

实现思路:

  1. 生成一个新的Promise。
  2. 遍历传入的promise数组,依次调用每一个promise的then方法注册回调。
  3. 在then 回调中把promise返回值push到一个结果数组中,检测结果数组长度与promise数组长度相等时表示所有promise都已经resolve了,再执行总的resolve。

Promise.race则更简单,只有任意一个promise fulfilled就执行总的resolve。

代码如下:

static all(list){
      return new PromiseA(function (resolve){
        var results=[];
        list.forEach((promise)=>{
            promise.then((val)=>{
                results.push(val);
                if(results.length==list.length){
                    resolve(results);
                }
            })
        })
      })
  }
复制代码

写好之后运行,可以得到并行的结果,但是还存在一点问题,Promise.all resolve后返回的结果数组顺序不能保证和传入的promise一致, 感谢tk103331的提出,因此还需要改写一下,上面的代码是用push,因此顺序是按照时间顺序往后追加结果的,如果传入的数组下标直接给结果数组赋值,就可以解决此问题。

修改后的代码如下:

static all(list){
      return new PromiseA(function (resolve){
        var results=new Array(list.length);
        var completeCount=0;
        list.forEach((promise,index)=>{
            promise.then((val)=>{
                results[index]=val;
                completeCount++;
                if(completeCount==list.length){
                    resolve(results);
                }
            })
        })
      })
  }
  static race(list){
    return new PromiseA(function (resolve){
        list.forEach((promise)=>{
            promise.then((val)=>{
                resolve(val);
            })
        })
    });
  }
}
复制代码

我们写个测试用例跑一下

console.time("all");  
var p1=new PromiseA(function (resolve){
    setTimeout(function (){
        let msg="promise 1111"
        console.log(msg);
        resolve(msg);
    },5000)
})
var p2=new PromiseA(function (resolve){
    setTimeout(function (){
        let msg="promise 2222"
        console.log(msg);
        resolve(msg);
    },2000)
})

var p3=new PromiseA(function (resolve){
    setTimeout(function (){
        let msg="promise 3333"
        console.log(msg);
        resolve(msg);
    },3000)
})

PromiseA.all([p1,p2,p3]).then(function (results){
    console.timeEnd("all");
    console.log(results)
})

复制代码

执行结果,能够保证顺序了。

小结

盘点了一下,一共用了90多行代码,实现了Promise A+的绝大部分功能,业务上能用到的所有功能都已经实现,但是有一些语法糖和容错处理没有做,尚不能跑通Promise规范的所有用例。目前主流浏览器和node.js都已实现了es6 promise规范,所有第三方的promise库都失去了存在的意义, 本文代码仅限于技术研究,深入理解原理。

最后,推广一下个人的开源项目,node.js web开发框架: webcontext

传送门:github.com/windyfancy/…