讲给小白听的Promise原理剖析

2,254 阅读8分钟

本文想从一个全新的角度来理解Promise的实现原理,即通过Promise的一些外在表现,一步步的去实现一个符合Promise所有行为的Promisee。

虽然说是针对小白用户的,但是如果对Promise的使用一无所知的话,恕老夫也无能为力了,还是先去学习一下Promise的用法吧。

下面我们根据promise的一些测试用例慢慢完善我们自己的Promise,希望读者能打开编辑器,跟着本文的步骤一起去敲一下代码。看看为了实现Promise的每个行为都做了哪些工作,这样到最后Promise内部的运转机制就尽在我们掌握了。我们姑且把咱们要实现的Promise叫做MyPromise吧。
首先我们新建一个MyPromise构造函数.一开始MyPromise是一个空函数。

function MyPromise(fn){

}  

这里fn是在创建Promise实例时使用者传给Promise的回调函数,我们知道fn里会有两个参数resolve和reject。

0、测试fn同步执行

我们知道Promise接收的回调fn是同步执行的,以下代码会先打印1,再打印2

 //0 测试fn同步执行
var a = new Promise((resolve, reject) => {
    console.log(1)
});
console.log(2)

那么我们的MyPromise改为

function MyPromise(fn){
    fn()
} 

这样我们的MyPromise就具备了promise同步执行的行为。

刚才说了fn接收resolve和reject两个参数,我们就在MyPromise内部定义两个函数分别叫resolve和reject,而且把这俩当参数传给fn调用。
Promise的实例有一个then参数,我们一块给定义了。

function MyPromise(fn){

    function resolve(){

    }
    function reject(){

    }

    fn(resolve,reject);

    this.then=function(onFullfilled,onRejected){}
} 

1、测试resolve,reject,then

在resolve时会调用传给then的第一个函数,即成功回调,并把resolve的值传给成功回调。
在reject时会调用传给then的第二个函数,即失败回调,并把reject的值传给失败回调。

//1 测试resolve
var a = new Promise((resolve, reject) => {
    resolve('success');
});
a.then((val) => {
    console.log(val) //打印success
})

//2 测试reject
var b = new Promise((resolve, reject) => {
    reject('error');
});
b.then((val) => {
    console.log(val)
}, (error) => {
    console.log(error) //打印error
})

我们定义一个数组deffers来保存then函数传入的成功失败回调,并在resolve或者reject的时候有选择性的执行其中一个。 这里还声明了value和status两个变量,value代表当前Promise最终改变的值,是resolve的值或者reject的error。status代表Promise的状态,最初是pending,用null表示。reoslve了改变为true,reject的话改为false。只能改变一次,无论转成true还是false都不能再改变了。

function MyPromise(fn){
    var value; //resolve或者reject的值,在resolve或者reject时改变
    var status=null; //该Promise的状态,null:初始,  true:成功(resolve), false:失败(reject)
    var deffers=[] //回调数组,每调用一次then,
    //就往里push一个{onFullFilled:onFullFilled,onRejected:onRejected}的回调对。
    //并在resolve或者reject时遍历调用每一个onFullFilled或者onRejected函数

    fn(resolve,reject);

    function resolve(val){
        value=val;
        status=true;
        final()
    }
    function reject(val){
        value=val;
        status=false;
        final()
    }

    //遍历执行deffers里的函数
    function final(){
        for(var i=0,len=deffers.length;i<len;i++){
            handle(deffers[i])  //deffers[i]=>{onFullFilled:onFullFilled,onRejected:onRejected}
        }
    }

    //真正的处理结果的函数,根据status的值来判断
    function handle(deffer){ //deffer=>{onFullfilled:onFullfilled,onRejected:onRejected}
        if(status===true){
            deffer.onFullfilled && deffer.onFullfilled(value)
        }

        if(status===false){
            deffer.onRejected && deffer.onRejected(value)
        }

    }

    this.then=function(onFullfilled,onRejected){
        var deffer = {onFullfilled:onFullfilled,onRejected:onRejected}
        deffers.push(deffer);
        handle(deffer);
    }
} 

2 测试异步的resolve和异步的reject

 //3 测试异步的resolve
var b = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('success');
    }, 1000);
});
b.then((val) => {
    console.log(val, ' async')  //1s后打印 success async
}, (error) => {
    console.log(error, ' async')
})

//4 测试异步的reject
var b = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('error');
    }, 1000);
});
b.then((val) => {
    console.log(val, ' async')
}, (error) => {
    console.log(error, ' async') //1s后打印 error async
})

这部分的代码逻辑上一步已经实现了,不再赘述

3 测试resolve和reject都调用的情况

即使一个Promise的resolve和reject都调用,Promise的状态会根据调用顺序只改变一次,后边的resolve或者reject是无效的。

//5 测试resolve 和 reject都会调用的情况 先调用resolve
var b = new Promise((resolve, reject) => {
    resolve("success");
    reject('error'); 
});
b.then((val) => {
    console.log(val, '同时调用')  //打印 success 同时调用
}, (error) => {
    console.log(error, '同时调用')
})

  //5 测试resolve 和 reject都会调用的情况 先调用reject
var b = new Promise((resolve, reject) => {
    setTimeout(()=>{
        resolve("success");
    },100)
    reject('error'); 
});
b.then((val) => {
    console.log(val, '同时调用')  
}, (error) => {
    console.log(error, '同时调用') //打印 error 同时调用
})

我们的MyPromise实现如下,只贴修改的部分

function MyPromise(fn){
    ...

    //fn(resolve,reject); 
    doResolve(fn,resolve,reject) //不调用fn,改为调用doResolve,doResolve里调用fn

    function doResolve(fn,resolve,reject){
        var hasChange =false;
        fn((value)=>{
            if(hasChange){
                return;
            }
            hasChange=true; //开关,调用一次resolve或者reject之后就设置为true,
            //下次就不会进来啦。下面同理
            resolve(value)
        },(error)=>{
             if(hasChange){
                return;
            }
            hasChange=true;
            reject(value)
        })
    }
} 

4 测试同一个promise调用多个then

Promise可以调用多次then,就像上文所说,这些回调都被放入了Promise内的deffers数组,并在resolve或者reject时遍历deffers依次调用,这部分的逻辑上文已经完成。

//6 测试同一个promise调用多个then
var b = new Promise((resolve, reject) => {
    resolve('success')
});
b.then((val) => {
    console.log(val, ' then1')
}, (error) => {
    console.log(error, ' then1')
})

b.then((val) => {
    console.log(val, ' then2')
}, (error) => {
    console.log(error, ' then2')
})

5 测试连续调用then的情况

Promise调用then方法的返回值是一个新的Promise,这个新Promise会接收前一个Promise的then回调里的成功回调返回值调用resolve,或者失败回调返回值调用reject,因此

var b = new Promise((resolve, reject) => {
    resolve('success')
});
var newP = b
            .then((val) => {
                console.log(val, ' then1');
                return '进入第二个then的成功回调' //如果b被resolve了,newP就会resolve 这个回调的返回值
            }, (error) => {
                console.log(error, ' then1');
                return '进入第二个then的失败回调' //如果b被reject了,newP就会reject 这个回调的返回值
            });

newP.then((val) => {
    console.log(val, ' then2');
}, (error) => {
    console.log(error, ' then2');
})

这里我们需要修改一下then方法

function MyPromise(fn){
    ...

     this.then=function(onFullfilled,onRejected){
        return new MyPromise((resolve,reject)=>{
            let deffer = {onFullfilled:onFullfilled,onRejected:onRejected,resolve:resolve,reject:reject};
            deffers.push(deffer)
            handle(deffer)
        })
    }
}

这里有几处修改,第一点是让then方法返回一个新的MyPromise,第二个是deffers数组里的每一个对象都缓存了新MyPromise的resolve和reject,这样前一个MyPromise的成功回调执行完了我们可以取出新MyPromise的resolve执行,这样就会形成一个管道了。reject同理。
下面我们来处理MyPromise链式调用的逻辑,主要集中在handle函数里

function MyPromise(fn){
    ...
    //真正的处理结果的函数,根据status的值来判断
    function handle(deffer){ //deffer=>{onFullfilled:onFullfilled,onRejected:onRejected,resolve:resolve,reject:reject} 这里的deffers缓存了新Promise的resolve和reject
        //cb是处理函数,根据status来取
        var cb = status===true?deffer.onFullfilled : deffer.onRejected;
        //如果没有传处理函数,比如resolve却没有传成功回调,我们就执行新Promise的resolve,把成功结果往后传递.
        if(cb==null){
            status==true?  deffer.resolve(value):deffer.reject(value);
            return;
        }
        //下面是有cb的逻辑
        var val;
        //这里用try catch的原因是成功和失败回调是使用者定义的,执行可能会报错,一旦报错就进入下个Promise的reject流程
        try{
            val=cb(value); 
        }catch(e){
            deffer.reject(e);
            return ;
        }
        //这里只要成功拿到val值,不论cb是当前Promise的成功处理函数,还是失败处理函数,只要当前Promise处理了,就进入下个Promise的resolve流程
        deffer.resolve(val)
    }
}

到这里,结合then方法和handle方法,MyPromise可以链式调用的逻辑就处理完了。handle里处理了then调用时不传回调的情况,也处理了传的回调函数执行报错的情况。所以以下测试是通过的

// 连续调用then
var b = new MyPromise((resolve, reject) => {
    resolve('success')
});
b.then(value=>{
    console.log(value,' then1 连续调用then'); //这里会打印,value='success'
    return 'new data'
}, (error) => {
    console.log(error, ' then1')
}).then(value=>{
    console.log(value) // 这里会打印,value='new data'
})

// 测试then里不写某个回调
var b = new MyPromise((resolve, reject) => {
    resolve('success')
});
b.then(null, (error) => {
    console.log(error, ' then1')
}).then((val) => {
    console.log(val, ' then2 then1没有成功回调') //这里会打印,因为前一个MyPromise没有传成功回调,就会进入这里
}, (error) => {
    console.log(error, ' then2 then1没有成功回调')
});



var b = new MyPromise((resolve, reject) => {
    reject('error')
});
b.then((value) => {
    console.log(value, ' then1')
}, null).then((val) => {
    console.log(val, ' then2 then1没有失败回调')
}, (error) => {
    console.log(error, ' then2 then1没有失败回调') //这里会打印,因为前一个MyPromise没有传失败回调,就会进入这里
});

//测试回调里报错
var b = new MyPromise((resolve, reject) => {
    resolve('error')
});
b.then((value) => {
    throw "抛个错误";
    return 333;
}).then((val) => {
    console.log(val, ' then2 then1执行报错')
}, (error) => {
    console.log(error, ' then2 then1执行报错') //这里会打印,因为then1里的成功回调执行时报错了
});

到这里我们关于Promise的逻辑基本就完成了。等等,还有种情况,有时我们会resolve一个新的Promise,或者在then的回调里又返回一个新的Promise,这个该怎么解决呢?

6 测试resolve一个Promise或者then的回调返回新Promise的情况

下面我们就来处理这个问题,这部分逻辑主要在MyPromise内部定义的resolve方法里,这里的resolve的参数value有可能是个新的Promise

function MyPromise(fn){
    ...
    function resolve(val){
        //这里用鸭式判别法来断定val是个Promise对象,只要有then方法就可以了
        if (val && (typeof val === "object" || typeof val === "function")) {
        var then = val.then;
        if (typeof then === "function") {
          //如果val是个Promise对象,就把当前Promise的resolve和reject当做回调传给val的then,
          //等val本身状态变更时自会调用resolve和reject,这样就能拿到val最终要传给我们的值啦
          doResolve(then.bind(val), resolve, reject);
          return;
        }
      }
      //走到这里说明val已经是个非Promise对象了
      value=val;
      status=true;
      final()
    }
}

以上就是对Promise对象的解析,当然还有一些细节还需要完善。如果能看到这里并且看明白了,以后再用Promise就可以做到知其然并知其所以然了。