Promise 源码解析

2,905 阅读9分钟

写promise的时候,发现自己应用了树,链表,前度遍历等方法,觉得对于部分想了解promise机制,并且加深数据结构学习的同学有些许帮助,决定写一篇文章分享出来。不过文笔实在堪忧,没有耐心看下去的同学,可以直接看源码。源码地址 .

promise的状态是什么时候变化的 ?

1:通过new Promise()生成的promise的状态变化过程:
当调用resolve时:

    情况1:
    promise1 = new Promise((resolve) => {
        setTimeout(() => {
            resolve('promise1');
        }, 1000)
    })

情况1: 当参数是一个非promise的时候,1秒后promise的状态立即变成resolve,并执行then里面的事件.

    情况2: 
    promise1 = new Promise((resolve) => {
        setTimeout(() => {
            promise2 = new Promise((resolve, reject) => {
                resolve('promise2');
            })
            resolve(promise2);
        }, 1000)
    })

情况2: 当参数是另一个promise的时候,这时promise1的状态由promise2来决定,什么时候promise2变化了状态,promise1的状态也会相应的变化,并且状态保持一致.

当调用reject时:

这里与resolve不同的是,reject不管参数是什么,状态都会立即变为reject。

2:通过then()或者catch()生成的promise的状态变化过程

    情况1:
    promise1 = new Promise((resolve) => {
        resolve('promise1');
    })
    promise2 = promise1.then((data) => {
        return 'promise2';
    })

情况1: 当回调函数里面直接return一个非promise,和上面的情况1一样,当前的promise2状态变为resolve。相当于执行了(resolve('非promise'))

    情况2:
    promise1 = new Promise((resolve) => {
        resolve('promise1');
    })
    promise2 = promise1.then((data) => {
        promise3 = new Promise((resolve, reject) => {
            resolve('promise3');
        })
        return promise3;
    })

情况2: 当回调函数里面直接return一个promise3,和上面情况2一样,当前promise2的状态依赖于primise3,相当于执行了(resolve(promise3))

    情况3:
    promise1 = new Promise((resolve) => {
        resolve('promise1');
    })
    promise2 = promise1.then((data) => {
        console.log( iamnotundefined );
    })

情况3: 当回调函数里面代码报错了,并且没有被catch到的,当前promise状态变为reject.(异步的error代码catch不到,不会影响promise状 态变化)

通过几个promise例子说明一下

promise1 = new Promise((resolve, reject) => {
    setTimeout(()=>{
        resolve('promise1_resolve_data');
    },1000)
})
console.log(promise1);
promise2 = promise1.then((data) => {
    console.log('promise2---', data);
    return 'promise2';
})
console.log(promise2);
promise3 = promise1.then((data) => {
    console.log('promise3---', data);
    return 'promise3';
})
console.log(promise3);
setTimeout(() => {
    console.log('--promise1--', promise1);
    console.log('--promise2--', promise2);
    console.log('--promise3--', promise3);
},3000)

代码执行结果:

依次输出promise1,promise2,promise3,状态都是pendding.一秒过后执行relove,promise1状态变为resolve,值为'promise1_resolve_data'.之后依次执行promise1.then里面的回调函数,promise2状态变为resolve,值为'promise2'.promise3状态变为resolve,值为'promise3'.

上面代码段看出了什么?

1:当初始化promise1,promise2,promise3后,三个promise的状态都是pendding.
2:当promise1里面的resolve执行后,promise1的状态立即变为resolve,值为resolve函数参数.
3:promise2,promise3都是通过promise1的then方法生成出来的,并且在promose1状态变为resolve之后也都依次状态变为了resolve。

通过上面的代码可以先得出的结论是:

(1) : 每一个promise的状态的变化都不是立即就变化得,而是在未来的某一个时刻变化的。这里可以想到:当我们自己实现的时候,一定要有一个结构去维护着所有promise.

(2) : 什么结构呢? 这里可以看出,promise2,promise3都是由promise1的then方法返回的,可以看出这是一个一对多的关系结构,所以这里的结构一定是一个树的结构。

(3) : 什么时候去'装载'每一个promise和相关的事件呢?很简单,then和catch方法里面。

(4) : 什么时候去'执行'promise状态变化,相关的事件回调? resolve,reject里面。

(5) : 说白了,也就是两个过程,装载过程(then,catch),执行过程(resolve,reject)

开始写代码

当去实现一个东东的时候,比如promise,首先要做的是熟悉promise的语法,特性。分析每一个promise之间的关系,然后才能确定一个合适的数据结构去存储它。前期的结构关系设计合理了,代码写起来也会很容易。

    只写核心代码
    function PP(){
        let promises = new Map(); // 存储所有的promise实例
        let index = 1;
        // Promise 构造函数
        function App(fn){
            this.state = "pendding";
            this.id = +index; //每个promise的唯一标识
            fn(this.resolve.bind(this), this.reject.bind(this));
        }
        return App;
    }
    代码很简单,不做解释

前面说到了,promise的实现其实就是两个过程,装载执行. 先说下装载过程,也就是then() catch()的实现

    只写核心代码
    App.prototype.then = function(resolve, reject){
        let instance = new App(()=>{}); // 生成一个初始状态的promise,并返回
        //把instance和相应的回调保存起来 
        /**
         * type: 用来判定这个promise是通过then方法创建的
         * instance: promise实例
         * callback: 保存的事件 
         */
        let item = {
            type : 'then', 
            instance : instance,
            callback : length > 1 ? ([{
                status : 'resolve',
                fn : resolveFn
            },{
                status : 'reject',
                fn : rejectFn
            }]) : ([{
                status : 'resolve',
                fn : resolveFn
            }])
        }
        // 这里通过map存储的,两个promise之间的关系就通过promise的_id相互关联.
        let p_item;
        if(p_item = promises.get(this._id)){
            p_item.push(item);
        }else{
            promises.set(this._id,[item])
        }
        return instance;
    }
    
    App.prototype.catch = function(rejectFn){
        // 和then差不多
        let instance = new app(()=>{});
        let item = {
            type : 'catch',
            instance : instance,
            callback : ([{
                status : 'reject',
                fn : rejectFn
            }])
        }
        let p_item;
        if(p_item=promises.get(this._id)){
            p_item.push(item);
        }else{
            promises.set(this._id,[item])
        }
        return instance;
    }

说下执行的过程 , resolve() , reject()的实现 。
辅助案例: 代码 2-1

    promise1 = new Promise((resolve, reject) => {
        setTimeout(()=>{
            resolve("resolve data from promise1");
        },1000)
    })
    promise2 = promise1.then((data) => {
        console.log('promise2---', data);
        return 'promise2';
    })
    promise3 = promise2.then((data) => {
        console.log('promise3---', data);
        return 'promise3';
    })
    promise4 = promise1.then((data) => {
        console.log('promise4---', data);
        return 'promise2';
    })
    promise5 = promise1.catch((data) => {
        console.log('promise4---', data);
        return 'promise2';
    })

这是代码2-1,promise之间的关系图

执行过程:一秒后执行了resolve方法,当前promise状态变为resolve.之后拿出与promise1下面的三个promise,分别是promise2,promise4,promise5.之后拿出每一个promise相关的事件,并执行。上面说了,像promise2,promise4,promise5这些通过then或者catch生成的promise,状态变化过程由返回值来决定。

    App.prototype.resolve = function(data){
        let ans = null; // 回调函数的结果
        let promise = null; //每一个子节点中的promise实例
        let items; //一个节点下面的子节点
        //执行mic任务队列里面的任务 , 这里用setTimeout(fn,0)代替
        setTimeout(() => {
            // 上面说到,这里做的事就是处理promise的变化。
            if(typeof data == 'object' && data!==null &&  data.__proto__.constructor == app){
                // 如果传入的参数是一个promise对象,这个时候当前的promise的状态不是立即变化的,而是依赖于传入的promise也就是data的变化而变化。
                // 所以这里要做的是就是关联这两个promise,这里我用的链表
                data.previous = this;
            }else{
              // 这里也就是上面说的情况1,resolve传入的参数是一个非promise,这个时候当前promise立即变化,并执行相关的事件回调.
              setTimeout(() => {
                this.state = "resolve";
                this.value = data;
                loadLineToList(this); // (很重要,单独解释2)
                //拿出当前节点下面的所有子节点
                if(items = promise.get(this._id)){
                    // 这里以2-1示例代码为例,分别拿出promise2,promise4,promise5 .
                    // 上面promise项里面的数据结构,分别是 type字段,instance字段,callback字段。在then,或者catch里面有写😊
                    // 拿出每一个promise的callback,并执行
                    for(let i=0;i<items.length;i++){
                        if(items[i].type == 'then'){
                            try{
                                ans = items[i].callback[0].fn(data);
                            }catch(err){
                                promise = promises.get(this._id)[i].instance;
                                promise.reject(err);
                                continue;
                            }  
                        }
                        //这里已经拿到了事件执行的结果,ans 
                        if(typeof ans == 'object' && ans!==null &&  ans.__proto__.constructor == app){
                            ans.previous = promise;
                        }else{
                            if(promise){
                                promise.resolve(ans);
                            }
                        }
                    }
                }else{
                    //下面没有节点了,出口
                    return;
                }
              },0)
            }
        },0)
    }

代码2-2,返回值都是非promise,处理过程如上。接下来说另一种情况,返回值是promise , loadLineToList()这个函数就是用来处理这种情况的

    promise1 = new Promise((resolve, reject) => {
        setTimeout(()=>{
            promise2 = new Promise((resolve) => {
                setTimeout(() => {
                    promise5 = new Promise((resolve) => {
                        resolve('promise5');
                    })
                    promise7 = promise5.then(() => {
                        
                    })
                    resolve(promise5);
                },1000)
            })
            console.log('1s');
            resolve(promise2);
        },1000)
    })
    promise3 = promise1.then((data) => {
        console.log(data);
        promise4 = new Promise((resolve) => {
            setTimeout(() => {
                resolve('promise4');
            },1000)
        })
        return promise4;
    })
    promise6 = promise3.then((data) => {
        console.log(data);
    })
    setTimeout(() => {
        console.log('--promise1--', promise1);
        console.log('--promise2--', promise2);
        console.log('--promise3--', promise3);
        console.log('--promise4--', promise4);
        console.log('--promise5--', promise5);
        console.log('--promise6--', promise6);
    },4000)

上面是代码2-2,promise关系图,分析出每一个promise之间的关系是重要的,只有明确了关系才能设计出合适的数据结构。

上面的代码说明了参数是promise的情况 , promise1的变化依赖于promise2, promise2的状态依赖于promise5. 同样的,promise3的状态依赖于promise4. 这里可以清晰的看出,promise之间的关系是单向的,1对1的,所以用链表是合适的。

App.prototype.then代码中的data.previous = this;ans.previous = promise;用来建立链表的。loadLineToList这个函数用来处理链表中promise之前的关系。保持promise1,promise2,promise5状态一致,并且把promise2,promise5下面的所有promise'移'到promise1的下面。

reject的实现

说reject实现之前,先说明下promise的catch机制。

    promise1 = new Promise((resolve, reject) => {
        reject('promise1');
    })
    promise2 = promise1.then(() => {
    
    });
    promise4 = promise1.then(() => {
    
    });
    promise3 = promise2.catch(() => {
    
    })

上面代码会报一个 Uncaught Error: (in promise) promise1,如果没有最后的promise3的catch,会报2个Uncaught Error: (in promise) promise1

promise之间的关系是树形的,当一个节点状态变成了reject,那么一定要在此节点的下面一条线路上,有一个节点去catch这个reject,不然就会报错。像上面的promise1变成了reject,会向下面的子节点去'发散',promise2没有catch,那么promise2的状态变成reject,并且继续向下找,promise3catch到了,然后结束。另一条线路,promise4没有catch到,状态变为reject,由于下面没有节点了,也就是没有catch,所以会抱一个Uncaught Error: (in promise) promise1

说清了catch机制,再去写reject相关的代码就容易了。

    App.prototype.reject = function(error){
        let promise = null; //子节点
        let fn = null;  //then or catch的回调函数
        setTimeout(() => {
            this.state = "reject";
            this.value = error;
            loadLineToList(this);
            let list = promises.get(this._id);//拿出当前节点下面的所有子节点
            //出口,没有找到,报错
            if(!list || list.length==0){
                throw new Error("(in promise) "+error);
            }
            for(let i=0;i<list.length;i++){
                promise = list[i].instance; // 从左的第一个子节点开始
                type = list[i].type;
                if(type == 'then'){   // 这个promise 是通过p1.then() 出来的 , 但是由于p1是reject , 所以当前promise转换成reject
                    //处理then有两个回调函数的情况,第一个回调函数相当于catch
                    if(list[i].callback.length == 1){
                        promise.value = error;
                        promise.reject(error);
                        continue;
                    }else{
                        fn = list[i].callback[1].fn;
                    }
                }
                // 拿到catch里面的fn
                if(!fn){
                    fn = list[i].callback[0].fn; 
                }
                
                let ans = null; // 回调函数的结果
                // catch回调函数里的代码,如果代码报错,当前promise变为reject
                try{
                    ans = fn(error);
                    fn = null;
                }catch(err){
                    promise.reject(err);
                    continue;
                }
                promise.value = ans;
                if(typeof ans == 'object' && ans!==null &&  ans.__proto__.constructor == App){
                    ans.previous = promise;
                }else{
                    if(promise){
                        promise.resolve(ans);
                    }
                }
            }
        }, 5)
    }

总结

其实看promise的实现,每一个promise之间的关系是通过树的结构相互联系的。实现也是分为两个过程,装载和执行。装载也就是构建树的过程,catch和then方法。执行就是通过resolve和reject方法前度遍历去找出下面的节点,改变每一个promise的状态,并执行相关的回调函数。