学习Promise后自己手写的Promise, 代码非常简单

172 阅读6分钟

在掘金学习多日Promise, 这两天也手写了一个Promise,大家帮忙看看是不是这样的, 直接上代码,都是普普通通的写法, 掘金的大佬们轻喷

因为涉及ajax的JSON文件读取,测试要放nginx或apache下用http访问


模拟思路:

1. 有两种方式得到一个仿Promise对象实例
   1.1 第一种 new 方法,这种实例的特点是异步执行会去调用resolve,reject方法修改状态, 而且得到的第一个仿Promise对象一定是这种方法创建的       
   1.2 第二种 .then 方法, 这种实例的特点是要根据上个方法自己去调用resolve,reject方法修改自己的状态
2. 每个仿Promise对象的_objthen属性都会指向由自己.then方法创建的实例
3. 当一个仿Promise实例修改完自己的状态后,就会根据this._objthen属性去调用下一个对象修改自己的状态,由new创建的实例除外, 因为由new创建的实例会在异步后自己调用resolve,reject方法修改状态
4. 如果then中执行成功函数后返回的是一个仿Promise实例a,就把a._objthen属性也指向当前对象的_objthen, 这样当这个由new创建的实例a改变状态时,依然能调起a._objthen去修改状态了

未命名文件 (1)2.png

Paotui.js


class Paotui {
    static PAOTUI_ID = 1;
    static CBID = {RES:0,REJ:1,CAT:2,FIN:3,OTH:4};
    constructor(confun){
        this.id = Paotui.PAOTUI_ID++; //给每个Paotui实例对象一个id,便于调试跟踪区别
        this.status = 'pending';//状态 pending fulfilled rejected
        this.result = undefined;//存储结果
        this._objthen = null;//存储当前对象.then产生的对象
        this._callback = [,,,,[]];//存储当前对象的then方法的回调函数,便于后面回调 0:成功函数,1:失败函数,2:catch函数 3:finally函数 4:数组,存储其他回调函数
        
        if(confun){
            try {
                confun(this._resolve.bind(this),this._reject.bind(this));
            } catch(err) {
                this._reject.call(this, err);
            }
        }
    }

    //在状态变化时执行其他回调函数, 把当前的状态和结果一并传过去
    runothercallback(status, result) {
        
        while(this._callback[Paotui.CBID.OTH].length > 0) {
            //用shift弹出第一个数组,这时得到一个函数并执行,把status,result传过去
            this._callback[Paotui.CBID.OTH].shift()(status, result);
        }

    }
    
    //回调函数 包括0:成功函数,1:失败函数,2:catch函数 3:finally函数
    runcallback(status, result) {
        //最后一个对象_objthen为null, 这里先判断一下
        if(!this) return;
        
        //如果上一个对象的status为_resolved, 这里也执行成功函数, 就是_callback[Paotui.CBID.RES]中的内容
        if(status === 'fulfilled') {

            //调用_resolve 改变当前实例对象的状态, 状态跟着上个实例来
            this._resolve(result);
            
            //执行then方法传入的成功后执行函数, 
            //_callback[Paotui.CBID.RES] 这个函数有3种返回值 1 undefined 2 返回一个Paotui对象 3 其他基本类型的值或者非Paotui对象的值
            const callresult = this._directExecution(this._callback[Paotui.CBID.RES], result) || result;
            
            //如果返回值是一个新的Paotui对象, 就把当前对象的_objthen挂载到新对象上,这样新对象的状态改变后就可以自动回调then的_callback了
            if(callresult instanceof Paotui) {
                callresult._objthen = this._objthen; 
            } else {
                
                //如果是基本类型或者非Paotui对象
                //这种对象是不会调用Paotui对象的构造函数的_resolve函数,就是说会一直是pending状态
                //返回值既然不是Paotui对象,也就不存在异步操作, 所以我们直接手动调用this._objthen的_callback中的函数去改变它的状态
                this.runcallback.call(this._objthen, status, callresult);
            }
            
        } else if(status === 'rejected') {
            
            this._reject(result);

            const callresult = this._directExecution(this._callback[Paotui.CBID.REJ], result) || result;            

            //如果有catch的话
            this._directExecution(this._callback[Paotui.CBID.CAT], result);
            //继续调用下一个Paotui对象
            this.runcallback.call(this._objthen, status, callresult);       

        }

        //不管状态是什么,回调finally(如果有的话) finally不支持传参数
        this._directExecution(this._callback[Paotui.CBID.FIN]);
        
    }

    
    //then方法,直接创建一个新的Paotui对象实例,然后返回这个对象
    //后面的回调函数会去修改这个对象,只要保持这个对象不指向其他地方就行
    then(resfun, rejfun){
        const newpt = new Paotui();

        //把新创建的Paotui对象地址保存的当前对象的_objthen变量中
        this._objthen = newpt;

        //把resfun 和 rejfun 两个函数放入_callback数组的0,1位置, 在后面当前对象状态被改变时执行
        newpt._callback[Paotui.CBID.RES] = typeof resfun === 'function' ? resfun.bind(newpt) : undefined;
        newpt._callback[Paotui.CBID.REJ] = typeof rejfun === 'function' ? rejfun.bind(newpt) : undefined;

        return newpt;
    }
    
    //把状态修改为fulfilled
    _resolve(result) {
        if(this.status !== 'pending') return;
        
        this.status = 'fulfilled';
        this.result = result;
        console.timeLog("paotuitime",'对象id=',this.id," 状态改为 fulfilled");
        //如果this._callback[Paotui.CBID.RES] === undefined && this._callback[Paotui.CBID.REJ] === undefined说明这个对象是由构造函数new创建的
        //因为由then中创建的Paotui对象_callback中至少会有一个回调函数
        //当前实例状态一旦改变,就去调用关联的then对象的_callback
        //then中创建的的对象这里不管, 因为在runcallback中会去回调, 也可以多设置一个变量达到这个目的
        if(this._callback[Paotui.CBID.RES] === undefined && this._callback[Paotui.CBID.REJ] === undefined) {
            //这里用call传入当前对象的then创建的对象绑定为runcallback的this, 传入当前的状态和result
            this.runcallback.call(this._objthen, 'fulfilled', result);
        }

        this.runothercallback('fulfilled', result);
    }
    
    //把状态修改为rejected
    _reject(result){
        if(this.status !== 'pending') return;

        this.status = 'rejected';
        this.result = result;
        console.timeLog("paotuitime",'对象id=',this.id," 状态改为 rejected");
        if(this._callback[Paotui.CBID.RES] === undefined && this._callback[Paotui.CBID.REJ] === undefined) {
            //这里用call传入当前对象的then创建的对象绑定为runcallback的this, 传入当前的状态和result
            this.runcallback.call(this._objthen, 'rejected', result);
        }

        this.runothercallback('rejected', result);
    }

    //直接执行函数
    _directExecution(executionFun, val) {
        
        if(typeof executionFun === 'function') {
            const exeresult = val ? executionFun(val) : executionFun();
            executionFun = null;            
            return exeresult;
        }        

    }

    catch(errorFun) {
        this._callback[Paotui.CBID.CAT]= errorFun.bind(this);
        return this;
    }

    finally(finallyFun) {        
        this._callback[Paotui.CBID.FIN] = finallyFun.bind(this);
    }

    
    //传入Paotui实例直接返回, 否则返回一个状态为fulfilled, 结果为val的Paotui实例
    static resolve(val, otherCallback) {
        
        if(val instanceof Paotui) {
            return val;
        }
        
        if(typeof(val?.then) === 'function') {
            const newpt = new Paotui(val.then);
            return newpt;
        }

        const newpt = new Paotui();
        if(otherCallback) newpt._callback[Paotui.CBID.OTH].push(otherCallback);
        newpt._resolve(val);
        return newpt;
    }

    //所有都成功才叫成功, 只要有一个失败就叫失败
    static all(paotuis) {
        let wccount = 0;
        const arrayResult = [];

        const allCallback = function(len, curindex, status, result) {

            if(this.status !== 'pending') return;
            wccount++;

            //只要有一个失败就叫失败
            if(status === 'rejected') {
                this._reject(result);
                return;
            }

            arrayResult[curindex] = result;

            if(wccount === len) this._resolve(arrayResult);
        }
        
        const newpt = Paotui.bindothecallback(paotuis, allCallback);
        console.timeLog("paotuitime",`newpt对象建立成功,ID= `, newpt.id);
        return newpt;
    }

    //第一名成功就成功, 第一名失败就失败
    static race(paotuis) {

        const raceCallback = function(len, curindex, status, result) {
            if(this.status !== 'pending') return;

            if(status === 'fulfilled') {
                this._resolve(result);
                return;
            }
            this._reject(result);
            
        }
        
        const newpt =  Paotui.bindothecallback(paotuis, raceCallback);
        console.timeLog("paotuitime",`race的newpt对象建立成功,ID= `, newpt.id);
        return newpt;
    }

    //只成功, 但必须要等所有状态都改变后, 没有失败
    static allSettled(paotuis) {
        let wccount = 0;
        const arrayResult = [];

        const allSettledCallback = function(len, curindex, status, result) {
            if(this.status !== 'pending') return;
            wccount++;
            arrayResult[curindex] = {status: status, value: result};

            if(wccount === len) this._resolve(arrayResult);
            
        }
        
        return Paotui.bindothecallback(paotuis, allSettledCallback);
    }

    //只要一个成功就叫成功, 所有都失败才能叫失败
    static any(paotuis) {
        let wccount = 0;
        const arrayResult = [];

        const anyCallback = function(len, curindex, status, result) {
            if(this.status !== 'pending') return;
            
            wccount++;            

            if(status === 'fulfilled') {
                this._resolve(result);
                return;
            }

            arrayResult[curindex] = result;

            if(wccount === len) this._reject(arrayResult);
            
        }
        
        return Paotui.bindothecallback(paotuis, anyCallback);
    }

    //绑定其他回调函数
    static bindothecallback(paotuis, callbackfun) {

        const newPaotuis = Array.isArray(paotuis) ? paotuis : [...paotuis.values()];
        
        const newpt = new Paotui(), len = newPaotuis.length;

        if(len === 0) { //如果newPaotuis为空
            newpt._reject.call(newpt,[]);
            return newpt;
        }

        for(let it=0;it<len;it++) {
            if(newPaotuis[it] instanceof Paotui) {                
                newPaotuis[it]._callback[Paotui.CBID.OTH].push(callbackfun.bind(newpt, len, it));
            } else {
                newPaotuis[it] = Paotui.resolve(newPaotuis[it], callbackfun.bind(newpt, len, it));
            }
            
        }
        return newpt;
    }

}










测试用index.html


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>跑腿</title>
    <script src="http://upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.0.min.js"></script>
    <script src="./paotui.js"></script>
    <script src="./MyPromise.js"></script>
  </head>
  <body>
    <div>
<fieldset>
    <legend></legend><select id="startfilename"><option>data1.json</option><option>data2.json</option><option>data3.json</option><option>data4.json</option><option>nofile.json</option></select>文件开始<br>
    <br><select id="witchpromise">
      <option value="Paotui">Paotui</option>
      <option value="Promise">Promise</option>
      <option value="MyPromise">MyPromise</option>
    </select>方法<br>
    <br><select id="numcggl">
      <option value="10" selected>10</option>
      <option value="9" selected>9</option>
      <option value="8">8</option>
      <option value="7">7</option>
      <option value="6">6</option>
      <option value="5">5</option>
      <option value="4">4</option>
      <option value="3">3</option>
      <option value="2">2</option>
      <option value="1">1</option>
      <option value="0">0</option>

    </select>成成功概率<br>
    
  </fieldset>
  <br>
  <br>
  <fieldset>
    <legend></legend>
    <button onclick="testThen();">测试Paotui.then</button><br>

    <br><button onclick="testAll();">测试Paotui.all</button>所有都成功才叫成功, 只要有一个失败就叫失败<br>

    <br><button onclick="testRace();">测试Paotui.race</button>第一名成功就成功, 第一名失败就失败<br>

    <br><button onclick="testallSettled();">测试Paotui.allSettled</button>大家都动一下就叫成功, 没有失败<br>

    <br><button onclick="testAny();">测试Paotui.any</button>只要一个成功就叫成功, 所有都失败才能叫失败<br>
  </fieldset>
  </div>
    <script>

      const witchpromise = {"Paotui":Paotui, "Promise":Promise, "MyPromise":MyPromise}

      
      const getJsonData = function(url, data) {
          const promise = witchpromise[$("#witchpromise").val()];
          
          return new promise((resolve, reject) => {

              $.ajax({ type:'GET', url:url,  dataType:"json", data:data,
                success: function(res) {
                  const numrmd = Math.round(Math.random()*10);
                  
                  if(numrmd <= Number($("#numcggl").val())) {
                    setTimeout(() =>{
                      resolve(res);
                    }, Math.round(Math.random()*10)*100);
                    
                    //reject("第二次改执行reject" + url);
                  } else {
                    reject("哦或,随机reject了,随机数 = " + numrmd + " 文件名: " + url);
                  }
                  
              },
              error:function(res) {
                reject(res);
              }
            });

        });
      }

  function testThen() {
    console.log("******************开始了******************")
    const filename = $("#startfilename").val();

    console.log(`从${filename}开始`);
    console.log(`使用${$('#witchpromise').val()}方法`);
    console.time("paotuitime");
    const jd = getJsonData('./' + filename);
    
    jd.then(function(val){      

      console.log(" 第一层的结果: " , val);
      const pp =  getJsonData('./' + val.filename);

      return pp;
    })
    .then((val)=>{
      console.log(" 第二层的结果: " , val);
      const pp =  getJsonData('./' + val.filename);

      return pp;
    }).then((val)=>{      

      console.log(" 第三层的结果: " , val);
      return "这是最后一个结果了,OVER"
    })
    .then((val)=>{      

      console.log(" 最后一层的结果: " , val);
      
    }).catch(function(errmsg) {
      console.log(" catch的结果: " , errmsg);
    }).finally(function(){
      console.log(" 执行finally进行收尾");
    });

    setTimeout(()=>{console.timeEnd("paotuitime","计时关闭")},3000);
    console.log("******************结束了******************")
  }

  function testAll() {
    console.time("paotuitime");
    const promises = [7,6,5,4,3,2,1].map(function (id) {
      return getJsonData('./data' + id + ".json");
    });
    promises.push('file888.json');

    const PaoORPrimise = witchpromise[$("#witchpromise").val()];

    const pt = PaoORPrimise.all(promises);
    
    pt.then((val)=>{
      console.log(" all 成功了 => " , val);
    }).catch((val)=>{
      console.log(" all 失败了 => " , val);
    });
    console.log(promises,pt);
    setTimeout(()=>{console.timeEnd("paotuitime","计时关闭")},2000);
  }

  function testRace() {
    console.time("paotuitime");
    const promises = [7,6,5,4,3,2,1].map(function (id) {
      return getJsonData('./data' + id + ".json");
    });
    const PaoORPrimise = witchpromise[$("#witchpromise").val()];

    const pt = PaoORPrimise.race(promises);
    
    pt.then((val)=>{
      console.log(" race 成功了 => " , val);
    }).catch((val)=>{
      console.log(" race 失败了 => " , val);
    });

    console.log(promises, pt);
    setTimeout(()=>{console.timeEnd("paotuitime","计时关闭")},2000);
  }

  function testallSettled() {
    console.time("paotuitime");
    const promises = new Map();
    [7,6,5,4,3,2,1].map(function (id) {
      promises.set(id, getJsonData('./data' + id + ".json")) ;
    });

    const PaoORPrimise = witchpromise[$("#witchpromise").val()];

    const pt = PaoORPrimise.allSettled(promises);
    
    pt.then((val)=>{
      console.log(" allSettled 成功了 => " , val);
    });

    console.log(promises, pt);
    setTimeout(()=>{console.timeEnd("paotuitime","计时关闭")},2000);
  }

  function testAny() {
    console.time("paotuitime");
    const promises = new Set();
    [7,6,5,4,3,2,1].map(function (id) {
      promises.add(getJsonData('./data' + id + ".json"));
    });

    const PaoORPrimise = witchpromise[$("#witchpromise").val()];

    const pt = PaoORPrimise.any(promises);
    
    pt.then((val)=>{
      console.log(" any 成功了 => " , val);
    }).catch((val)=>{
      console.log(" any 失败了 => " , val);
    });

    console.log(promises, pt);
    setTimeout(()=>{console.timeEnd("paotuitime","计时关闭")},2000);
  }
  

    </script>
  </body>
</html>






data1.json

{
	"filename":"data2.json"
}

data2.json

{
	"filename":"data3.json"
}

data3.json

{
	"filename":"data4.json"
}

Then测试结果:

uTools_1655954510589.png

Video_22-06-23_11-59-43.gif

all测试结果:

uTools_1655946848687.png

race测试结果:

uTools_1655953819882.png