10分钟搞定面试难点之Promise原理

949 阅读8分钟

Promise标准

1.Promise规范

Promise规范有很多,如Promise/A,Promise/B,Promise/D以及Promise/A的升级版Promise/A+。ES6 中采用了Promise/A+ 规范。 什么是 Promise/A+ 规范,推荐一篇文章Promises/A+规范(中文)

2.Promise标准解读

1.一个promise的当前状态只能是pendingfulfilledrejected三种之一。状态改变只能是pendingfulfilled或者pendingrejected,并且状态改变不可逆的。 2.promisethen方法接收两个可选参数,表示该promise状态改变时的回调(promise.then(onFulfilled, onRejected))。then方法返回一个promisethen方法可以被同一个promise调用多次。

Promise的使用

const promise = new Promise((resolve) => {
    setTimeout(()=> {
        resolve(1);
    }, 2000);
});
promise.then(a=> alert(a));

上面这段代码的解读:

  • 构造函数接收一个executor立即执行函数;
  • executor立即执行函数接收一个resolve函数;
  • promise对象的then方法绑定状态变为fulfilled时的回调,then方法也可以被称为注册函数(这句话怎么理解呢?不着急,下面会解释);
  • resolve函数被调用时会触发then方法中注册的回调函数(这句话很重要,在我看来,能理解这句话,就能理解promise);

用大家都会用,那么,我们来实现一个最最最简单的promise

实现简单的promise

直接看下面这段代码

function MyPromise(executor){
    var self = this;
    this.status = "pending";
    this.data = "";
    this.resolveArr = [];
    this.rejectArr = [];
    function resolve(data){
        //问题:为啥要在这里做异步处理(如过现在不能理解,等看完一定能理解的)
        self.data = data
        setTimeout(function(){
            self.resolveArr.forEach((fn)=>{
                fn(data);
            });
        },0);
    }

    function reject(){}

    try{
        executor(resolve,reject)
    }catch(err){
        reject(err)
    }
}

MyPromise.prototype.then = function(resolveFn,rejectFn){
    this.resolveArr.push(resolveFn);
    this.rejectArr.push(rejectFn);
}

来解释一下上边这段代码

  • MyPromise接收了一个executor作为参数,在实例化的时候去执行;这个executor接收两个参数resolvereject(注释:和原生的Promise(function(resolve,reject){})是保持一致的);

  • 接下来看resolvereject这俩参数,在MyPromise函数体内,可以看到,这两个参数其实是定义好的,可以理解为一个成功回调和一个失败的回调,和原生的Promise保持一致(在接下来的讲解中,基本以成功回调的resolve来做说明);来看下resolve函数内部做了什么?发现resolve只做了一件事,遍历了函数体内定义的resolveArr,然后执行了resolveArr了每一项,并把data参数传递进去;

那么问题来了,既然resolveArr的每一项都可以执行,那就是函数咯,我们可以看到,在函数体内,我们只定义了一个空的resolveArr,那这个函数是怎么来的呢?

  • 在代码的最后面发现了一个挂载在原型上的then方法里面有用到这个数组(在文章的前面有说到,then方法可以理解为注册方法,怎么个注册法呢?),原来在这个then方法会接收两个函数类型的参数(和原生的Promise保持一致);并把第一个参数添加到resolveArr中,第二个参数添加到rejectArr中。这也就完成了注册!

说了那么多,来测试一下,是否有效:

var test = new MyPromise(function(resolve){
    setTimeout(()=>{
        resolve("测试");
    },1000);
});

test.then((data)=>{
    console.log(data);
});

运行结果如下:

可以看到1秒钟以后会打印出结果(由于技术原因,不会做gif图,见谅);

重点再次提醒:时刻记住一句话,resolve函数被调用时会触发then方法中注册的回调函数,从上面这个案例可以充分的体现;

理解了上面这个案例以后,我们来慢慢的做优化

优化一

上面提到,then方法返回一个promise。then 方法可以被同一个 promise 调用多次。来改写下then方法:

MyPromise.prototype.then = function(onResolved,onRejected){
    this.resolveArr.push(onResolved);
    this.rejectArr.push(onRejected);
    return this;
}

只需要在then的最后return this即可,看过jquery源码的同学应该都很熟悉吧! 问题点:这样做的话,所有then方法注册的函数所接收的值都是一样的,这显然是我们不能接收的,后面还会优化

优化二

上面提到,一个promise的当前状态只能是pendingfulfilledrejected三种之一。状态改变只能是pendingfulfilled或者pendingrejected。来改下函数体内的resolve方法:

function resolve(data){
  if(self.status==="pending"){
    self.status = "fulfilled";
    setTimeout(function(){
      self.resolveArr.forEach((fn)=>{
        fn(data);
      });
    },0)
  }
}

只需在遍历resolveArr前判断状态是否为pending,如果是,则改变状态为fulfilled,然后执行遍历,这里也解释了文章前面提到的一句话(promise对象的then方法绑定状态变为fulfilled时的回调);

针对优化一提出的问题,来做进一步的优化

根据promise/A+规范优化以上代码

文章开头对promise/A+规范的标准解读中提到(then方法返回一个promise),那我们就给then方法返回一个promise,来改写then方法:

MyPromise.prototype.then = function(onResolved,onRejected){
    var self = this;
    return new MyPromise(function(resolve,reject){
        self.resolveArr.push(function(){
            var x = onResolved(self.data);
            if(x instanceof MyPromise){  
                //当then返回的是一个MyPromise的时候,会把resove挂在到该MyPromise的resolveArr队列中,等待该MyPromise执行对应的resolve;     
                x.then(resolve,reject)
            }else{
                //当then的返回值不是一个MyPromise的时候,直接执行resolve跳到下一个then处理;
                resolve(x);  
            }
        });
    });
}

再次强调:resolve函数被调用时会触发then方法中注册的回调函数

提问:这里涉及了几个MyPromise实例?
答案:2个或者3个;
为什么呢?
来看一下,代码中有一句var x = onResolved(self.data),并在后面判断了x是不是属于MyPromise类型;从代码中也可以看出,x是函数onResolved的返回结果,那这个onResolved又是什么呢?原来就是调用then方法时所注册的函数;当x也是一个MyPromise实例的时候,这里就涉及了三个MyPromise实例,分别是(当前实例(this),then方法返回的实例,注册函数返回的实例(x));

分析

为了方便,我把当前实例称为Athen方法返回的实例称为B; 当A调用then方法的时候,直接返回了B,并在B初始化的时候,给AresolveArr加入一个匿名函数(这里记为AResolveFn),当A中的resolve执行的时候,会去执行这个AResolveFn,在这个AResolveFn中,会去执行我们传入then中的onResolved方法,并把返回结果记x

情况一:当x不为MyPromise

从代码中可以看出,在这种情况下,直接执行了resolve(x);这个resolveBresolve,那Bresolve执行了,就会触发B实例用then方法注册的方法,这样就实现了then的链式调用,并且把每次注册方法的返回值传下去啦!
测试:

var test = new MyPromise(function(resolve){
    setTimeout(()=>{
        resolve("测试");
    },1000);
});

test.then((data)=>{
    console.log(data);
    return "测试2"
}).then((data)=>{
    console.log(data);
});

测试结果:

打印结果没问题。

情况二:当x为MyPromise

从代码上看,当xMyPromise的时候,直接把Bresolve当做了x的注册方法;这样的话,只有当xresolve执行的时候,会触发xthen注册的方法,才会触发Bresolve,才会触发Bthen注册的方法;
流程就是:Aresolve --> Athen注册的方法 --> xresolve --> xthen注册的方法 --> Bresolve --> Bthen注册的方法,这样就实现了异步链式传递;

测试:

var test = new MyPromise(function(resolve){
    setTimeout(()=>{
        resolve("测试");
    },1000);
});

test.then((data)=>{
    console.log(data);
    return new MyPromise(function(resolve){
        setTimeout(()=>{
            resolve("测试2");
        },1000)
    })
}).then((data)=>{
    console.log(data);
});

测试结果:

由于没有做gif图,看不出效果,但是从图中的执行时间可以看出来,整个过程确实是执行了2秒多;

针对状态改变的优化

以上说到的都是正常的状态下;就是说,当你用then方法注册函数的时候,都是在pending状态;但是,有的特殊场景,在你用then方法的时候,状态已经是fulfilled,就像下面这样:

var test = new MyPromise(function(resolve,reject){
    setTimeout(function(){
        resolve("test")
    },1000);
});

setTimeout(function(){
    test.then(function(data){
        console.log(data);
    });
},2000)

在这种情况下,当调用resolve方法的时候,并没有用then方法注册函数,那么resolveArr必然是一个空数组;此时状态是fulfilled,不可能说你用then方法注册的时候,再给你执行一边,也违背了Promise的标准。

优化then方法

MyPromise.prototype.then = function(onResolved,onRejected){
    var self = this;
    if(self.status==="fulfilled"){
        return new MyPromise(function(resolve,reject){
            var x = onResolved(self.data);
            if(x instanceof MyPromise){
                x.then(resolve,reject)
            }else{
                resolve(x);
            }
        });
    }

    if(self.status==="pending"){
        return new MyPromise(function(resolve,reject){
            self.resolveArr.push(function(){
                var x = onResolved(self.data);
                if(x instanceof MyPromise){
                    x.then(resolve,reject)
                }else{
                    resolve(x);
                }
            });
        });
    }
}

then方法中做了一层判断,当状态是fulfilled的时候,不会执行resolve,当然也不会触发then注册的函数,这里的做法是跳过这一层,直接去执行onResolved,然后继续走下去;上面原理都理解的话,这里理解起来应该也不是特别困难,这里就不做过多解释了,实在理不清楚的话就留言给我吧!

原理到这里基本上就差不多了,这里再补充一个promise静态方法

promise静态方法

Promise中有许多静态方法,像Promise.all,Promise.race,那我们用以上自己实现的MyPromise来加一个静态方法试试:

MyPromise.all = function(promiseArr){
    return new MyPromise(function(resolve){
        var length = promiseArr.length;
        var resultIndex = 0;
        var resultArr = [];
        promiseArr.forEach((promise)=>{
            promise.then(function(data){
                resultArr.push(data);
                resultIndex++;
                if(resultIndex===length){
                    resolve(resultArr);
                }
            });
        });
    });
}

原理理解的话,这个实现起来也就不难了,这里就不讲解了,直接来测试一下吧:

var test1 = new MyPromise(function(resolve){
    setTimeout(function(){
        resolve("test1")
    },1000)
});
var test2 = new MyPromise(function(resolve){
    setTimeout(function(){
        resolve("test2")
    },1500)
});
var test3 = new MyPromise(function(resolve){
    setTimeout(function(){
        resolve("test3")
    },1200)
});

结果如下:

结果没有问题吧,返回了一个数组,很开心有不有!有兴趣的小伙伴可以跟着这个思路,去实现一下其他静态方法。

总结

关于promise的内容到这里就差不多了,平时也比较少发表文章,总怕自己总结的不好,希望能给大家带来帮助吧!