美文网首页
让你从头到尾把promise整的明明白白

让你从头到尾把promise整的明明白白

作者: KlivitamJ | 来源:发表于2019-05-09 23:21 被阅读0次

    最近一直私下在看Android项目,前端这一块没怎么仔细研究。昨天在写重构公司前端项目的时候,我发现一旦有异步的任务,脑海里面条件反射一般的出现promise的字样。重构的多了 心就就在纳闷:既然promise这么好用,我能不能自己手写一个promise呢?我思索了半天,按照自己的想法模拟了出来,但是和一位大佬交流的时候,他说我的写法并没有遵循Promise/A+规范。当时我心里是懵逼的,加班的时候就一直想写这个问题。

    一、 promise是什么?

    1.1、 为什么要用promise?

    我们都知道,在javascript里面所有的代码都是单线程执行的。由于这种“单线程”的弊端,所有javascript重的网络请求、浏览器事件等都只能用异步来执行。传统的异步实现的方式都是用回调函数来实现的,例如:

    request.onreadystatechange = function () {
        if (request.readyState === 4) {
            if (request.status === RESULT_OK) {
                return success(req.responseText);
            } 
            return fail(req.status);
        }
    }
    

    这样虽然比较直观,但是并不符合程序员的审美习惯,代码的复用性也很低。按照现在主流的代码风格来说,一份优秀的代码模版就应该是链表形式的。

    // Android写习惯了,一般封装网络都是okhttp 。不用理我 = =
    okhttp.request()
          .onSuccesul()
          .onFail();
    

    于是promise应运而生。

    1.2、 promise介绍

    promise的中文释意为:承诺。我觉得这个翻译很能凸显这个对象的含义。

    promise里面有三个状态:

    • pending:进行中
    • fulfilled: 已成功
    • rejected:已结束

    为什么说,承诺这个解释很能凸显promise对象的特性呢?我们来为您谈谈promise里面的两个特点:

    1、 对象不能被任何手段来更改。怎么解释呢?只要是你下了承诺,就不能受任何外界环境的干扰,只受到一步操作的结果来决定。
    2、 一旦状态改变,就不会再变,并且任何时候都可以得到这个结果。

    从上面的特性里面,是不是更加加深了对承诺的理解呢!

    二、 promise简单探究。

    前面说了这么多,都是傻把式,下面我们就用代码体验一把promise的快感吧 。

    new Promise((resolve,reject)=>{
        let randomNumber = Math.random()
        console.log(randomNumber)
        if(randomNumber>0.5){
            resolve('success!!!')
        }else{
            reject('fail!!!')
        }
    }).then(res=>{
        console.log(res);
    },error=>{
        console.log(error);
    })
    

    从上面的代码我们可以比较清楚的发现,Promise接收两个参数:resolve和reject。resolve函数的作用是,将Promise对象的状态从“进行中”变为“成功”( pending => resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“进行中”变为“失败”( pending=>rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

    并且在Promise对象实例生成后,可以用then来分别指定完成后的resolve和reject。并且then里面的reject是可选项。

    对于promise,我觉得主要有三个点需要给读者讲一下,来看下面一段代码:

    let promise =  new Promise((resolve,reject)=>{
        let randomNumber = Math.random()
        console.log(randomNumber)
        if(randomNumber>0.5){
            resolve('success!!!')
        }else{
            reject('fail!!!')
        }
    })
    

    我们发现单独运行这一段代码的话,console依然被打印出来了。这说明promise是创建的时候就被运行的。
    再来看一段代码:

    let promise =  new Promise((resolve,reject)=>{
        let randomNumber = Math.random()
        if(randomNumber>0.5){
            resolve('success!!!')
    
        }else{
            reject('fail!!!')
        }
        console.log(randomNumber)
    }).then(res=>{
        console.log(res);
    },error=>{
        console.log(error)
    })
    

    效果如下:


    效果

    由上面的现象我们可以轻易的看出来:resolve和reject后的代码一般都还会运行,如果你想避免这种情况,你可以结合return来使用,例如:

    return resolve('success!!!');
    return reject('fail!!!');
    

    三、 promise重要属性探究

    2.1、 then

    Promise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。then里面有两个回调函数,前者返回resolve的回调函数,后者是可选值。并且then返回返回一个新的promise,这样就能一直使用链式结构了。

    2.2、 catch

    catch可以看成是then(null/undefined, reject)的别名 专门用来指定错误发生时的回调函数。那么为什么要设计这个属性呢?主要有两个方面,首先来看下面一段代码:

    // 普通then
    .then(res=>{
        console.log(res);
    },error=>{
        console.log(error)
    })
    
    //catch
    .then(res=>{
        console.log(res);
    })
    .catch(error=>{
        console.log(error)
    })
    

    明显第二个相对前一个会更加优雅一点。

    我们再来看一段代码:

    //  普通then
    .then(res=>{
        throw Error("have exception")
        console.log(res);
    },error=>{
        console.log(error)
    })
    // catch
    .then(res=>{
        throw Error("have exception")
    }).catch(error=>{
        console.log(error)
    }) 
    

    运行上面的代码我们很清楚的发现catch能捕获到then的异常,但是then的reject回调里面并不能捕获到resolve的异常。这在一定程度上保证了代码的正常运行顺序。

    2.3、 finally

    finally是es2018才引入的属性,不管 Promise 对象最后状态如何,都会执行的操作。其本质也是then方法的特性。

    promise
    .finally(() => {
      // ...
    });
    
    // 等同于
    promise
    .then(
      result => {
        // ...
        return result;
      },
      error => {
        // ...
        throw error;
      }
    );
    

    2.4、 all

    Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。其基础的语法是:

    const p = Promise.all([p1, p2, p3]);
    

    Promise.all方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。
    关于p的状态,主要由接收的promise数组决定的
    (1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
    (2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

    这里值得我们注意的一点是,如果p1,p2,p3中有错误 并且有自己的catch方法的时候。会调用其自己的catch,当没有catch方法的时候才会抛给p的catch来处理

    2.5、 race

    Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。其基础的语法是:

    const p = Promise.race([p1, p2, p3]);
    

    和all方法不同的是:
    (1)只有p1、p2、p3的状态都变成rejected,p才会变成rejected
    (2) 如果p1、p2、p3中有一个状态变成fulfilled,p的状态就会变成fulfilled 并将首次变成fulfilled的返回值返回。

    四、 初次手写的promise

    前面大概都将所谓的promise的用法给描述了一遍,大概写一两个例子就能感受出promise的奥秘。那么如果让你自己来写一个promise,你应该如何来写呢?

    这是我下班后写的一个Promise:

    const PENDING = "pending";
    const RESOLVE = "resolve";
    const REJECTED = "rejected";
    function JPromise(fn){
        const that = this;
        that.state = PENDING;
        that.value = null;
        that.resolvedCallbacks = [];
        that.rejectedCallbacks = [];
    
        function resolve(value){
            if(that.state === PENDING){
                that.state = RESOLVE;
                that.value = value;
                that.resolvedCallbacks.map(cb=>cb(that.value));
            }
        }
        function reject(value){
            if(that.state === PENDING){
                that.state = REJECTED;
                that.value = value;
                that.rejectedCallbacks.map(cb=>cb(that.value))
            }
        }
        try{
            fn(resolve,reject)
        }catch(e){
            reject(e)
        }
    }
    
    JPromise.prototype.then = function(onFulfilled,onRejected){
        onFulfilled = typeof onFulfilled === 'function'?onFulfilled:v=>v;
        onRejected = typeof onRejected === 'function'?onRejected:r=>new Error(r);
        if(this.state === PENDING){
            this.resolvedCallbacks.push(onFulfilled)
            this.rejectedCallbacks.push(onRejected)
        }
        if(this.state ===RESOLVE){
            onFulfilled(this.value);
        }
        if(this.state === REJECTED){
            onRejected(this.value)
        }
    }
    

    然后运行一下:

    new JPromise((resolve,rejected)=>{
            resolve(1)
    }).then(res=>{
        console.log(res);
    },error=>{
      console.log(error);
    })
    

    然后自测了一下,完美能实现简单的promise。但是跟大佬交流之后才知道:原来promise有自己的一套标准,我这套代码虽然能够简单的实现promise的功能 但是没有达到那套标准。

    五、 符合规范的promise

    首先关于promise规范,我就简单的提一下吧:

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

    最后张贴一下 我改进的代码:

    const PENDING =  'pending';//初始态
    const FULFILLED =  'fulfilled';//初始态
    const REJECTED =  'rejected';//初始态
    function Promise(executor){
      let self = this;//先缓存当前promise实例
      self.status = PENDING;//设置状态
      //定义存放成功的回调的数组
      self.onResolvedCallbacks = [];
      //定义存放失败回调的数组
      self.onRejectedCallbacks = [];
      //当调用此方法的时候,如果promise状态为pending,的话可以转成成功态,如果已经是成功态或者失败态了,则什么都不做
      function resolve(value){ //2.1.1
        if(value!=null &&value.then&&typeof value.then == 'function'){
          return value.then(resolve,reject);
        }
        //如果是初始态,则转成成功态
        //为什么要把它用setTimeout包起来
        setTimeout(function(){
          if(self.status == PENDING){
            self.status = FULFILLED;
            self.value = value;//成功后会得到一个值,这个值不能改
            //调用所有成功的回调
            self.onResolvedCallbacks.forEach(cb=>cb(self.value));
          }
        })
    
      }
      function reject(reason){ //2.1.2
        setTimeout(function(){
          //如果是初始态,则转成失败态
          if(self.status == PENDING){
            self.status = REJECTED;
            self.value = reason;//失败的原因给了value
            self.onRejectedCallbacks.forEach(cb=>cb(self.value));
          }
        });
    
      }
      try{
        //因为此函数执行可能会异常,所以需要捕获,如果出错了,需要用错误 对象reject
        executor(resolve,reject);
      }catch(e){
        //如果这函数执行失败了,则用失败的原因reject这个promise
        reject(e);
      };
    }
    function resolvePromise(promise2,x,resolve,reject){
      if(promise2 === x){
        return reject(new TypeError('循环引用'));
      }
      let called = false;//promise2是否已经resolve 或reject了
      if(x instanceof Promise){
        if(x.status == PENDING){
          x.then(function(y){
            resolvePromise(promise2,y,resolve,reject);
          },reject);
        }else{
          x.then(resolve,reject);
        }
      //x是一个thenable对象或函数,只要有then方法的对象,
      }else if(x!= null &&((typeof x=='object')||(typeof x == 'function'))){
        //当我们的promise和别的promise进行交互,编写这段代码的时候尽量的考虑兼容性,允许别人瞎写
       try{
         let then = x.then;
         if(typeof then == 'function'){
           //有些promise会同时执行成功和失败的回调
           then.call(x,function(y){
             //如果promise2已经成功或失败了,则不会再处理了
              if(called)return;
              called = true;
              resolvePromise(promise2,y,resolve,reject)
           },function(err){
             if(called)return;
             called = true;
             reject(err);
           });
         }else{
           //到此的话x不是一个thenable对象,那直接把它当成值resolve promise2就可以了
           resolve(x);
         }
       }catch(e){
         if(called)return;
         called = true;
         reject(e);
       }
    
      }else{
        //如果X是一个普通 的值,则用x的值去resolve promise2
        resolve(x);
      }
    }
    //onFulfilled 是用来接收promise成功的值或者失败的原因
    Promise.prototype.then = function(onFulfilled,onRejected){
      //如果成功和失败的回调没有传,则表示这个then没有任何逻辑,只会把值往后抛
      //2.2.1
      onFulfilled = typeof onFulfilled == 'function'?onFulfilled:function(value){return  value};
      onRejected = typeof onRejected == 'function'?onRejected:reason=>{throw reason};
      //如果当前promise状态已经是成功态了,onFulfilled直接取值
      let self = this;
      let promise2;
      if(self.status == FULFILLED){
        return promise2 = new Promise(function(resolve,reject){
          setTimeout(function(){
            try{
              let x =onFulfilled(self.value);
              //如果获取到了返回值x,会走解析promise的过程
              resolvePromise(promise2,x,resolve,reject);
            }catch(e){
              //如果执行成功的回调过程中出错了,用错误原因把promise2 reject
              reject(e);
            }
          })
    
        });
      }
      if(self.status == REJECTED){
        return promise2 = new Promise(function(resolve,reject){
          setTimeout(function(){
            try{
              let x =onRejected(self.value);
              resolvePromise(promise2,x,resolve,reject);
            }catch(e){
              reject(e);
            }
          })
        });
      }
      if(self.status == PENDING){
       return promise2 = new Promise(function(resolve,reject){
         self.onResolvedCallbacks.push(function(){
             try{
               let x =onFulfilled(self.value);
               //如果获取到了返回值x,会走解析promise的过程
               resolvePromise(promise2,x,resolve,reject);
             }catch(e){
               reject(e);
             }
    
         });
         self.onRejectedCallbacks.push(function(){
             try{
               let x =onRejected(self.value);
               resolvePromise(promise2,x,resolve,reject);
             }catch(e){
               reject(e);
             }
         });
       });
      }
    }
    

    具体的代码细节呢?我在代码中已经做了标注,如果又不懂的可以私聊、微信 欢迎来扰。

    说在最后

    已经有好几个中午好怎么睡午觉了,这篇文章写的有点敷衍。本来下班的时候就8点半了,然后又整理/调试代码 整了半天,结果在写文章的时候想例子想个半天都想不出来。我明天会利用休息时间好好来修改一下这篇文章的,太困了 先这样吧,我洗澡去睡觉了。您如果对现在的这篇文章不太满意,就请过一天再来看。

    最后提一句,能不能给我涨点人气啊,写了一年多,还是这点人气..

    相关文章

      网友评论

          本文标题:让你从头到尾把promise整的明明白白

          本文链接:https://www.haomeiwen.com/subject/xceygqtx.html