IT技术博客大学习 共学习 共进步

从Promise的Then说起

腾讯AlloyTeam 2016-03-29 23:34:32 浏览 1,342 次

Promise让代码变得更人性化

曾经我一直在思考,为什么代码会比较难读。后来发现,我们平时要阅读的所有媒体:报纸、书、新闻,我们在阅读的时候,都是从上到下一直读下去的,然而,我们的在读代码的时候,经常要跳着去读,这种阅读方式其实是反人类的,如果我们能在读代码的时候,也可以从上往下一直读下去,那么,代码就会变得可读性提高很多。

对比JS中,callback是让我们跳来跳去读代码最大的罪魁祸首,它让我们的流程变得混乱,Promise正是为了解决这一问题而产生的。我们来对比一下这个过程

varrender=function(data){
};
 
vargetData=function(callback){
    
    $.ajax({
        success:function(){
               callback(data);
        }
    });
};
 
varinit=function(){
    getData(function(data){
         render(data);
         
         getData(function(data){
             render(data);
         });
    });
};
 
init();

使用Promise之后

varinit=function(){
    getData({
 
    }).then(function(data){
 
       render(data);
       returngetData({});
    }).then(function(data){
 
       render(data);
    });
};

很明显看出,代码就变成线性的了,逻辑也变得更加清晰可读

Promise流程再优化

promse出来之后,大家都有很多的想法,在Promise之上再封装一层,使用异步流程更清晰可读。下面是Abstract-fence 2.0(开发中)的一种解决方案(规范)

Abstract-fence中,function会被分解为多个task

Model.task('getData',function(scope,next,{$,util}){
      $.ajax({
          success:function(data){
              next();
          }
      });
});
 
Model.task('render',['getData'],function(scope){
      vardata=scope.data;
      // 使用data进行渲染
});
Model.task('init',[render].then(render));
 
Model.runWorkflow(init);

其中, init是task render执行后再执行render, 而render任务又是由getData任务执行后再渲染组成,其中每个task的定义function的参数使用依赖注入传递,全局属性使用{}包裹

但是在使用Promise.all的过程中,遇到了一个Promise奇怪的问题

Array.prototype.then与Promise.all

很简单的一段代码

varp=newPromise(function(rs,rj){
    setTimeout(rs,1000);
});
 
Promise.all([p]).then(function(){
    console.log(2);
});
 
console.log(1);

毫无疑问,这段代码在浏览器运行会先打印1,然后再输出2 但如果在前面增加对then方法的定义,如下代码

Array.prototype.then=function(){
};
varp=newPromise(function(rs,rj){
    setTimeout(rs,1000);
});
 
Promise.all([p]).then(function(){
    console.log(2);
});
 
console.log(1);

那么这段代码只会打印出1, 2却永远不会运行

查了很多资料,确认promise.all的参数只能接收数组(类数组)

比如如下代码就会报错

varp=newPromise(function(rs){});
 
Promise.all(p);// 报错
 
Promise.all([p]);// ok

所以,Promise.all接收一个Iterator可遍历对象

对数组的prototype.then定义为什么会影响到Promise的行为呢?

Promise A+规范

Promise A+规范看起来还是有点绕,这里省略掉一些具体的实现细节,将Promise A+更直白的阐述如下

1. Promise then方法需要return一个新的Promise出来,如下

.then=function(rsFunc,rjFunc){
 
    varpromise2=newPromise();
    returnpromise2;
};

2. 如果promise本身状态变更到fulfilled之后,调用rsFunc,rsFunc的解析值x, 与新的promise2进行promise的解析过程[[Resolve]](promise2, x), x的取值不同,有不同的情况

3. 若x为一个promise,则x完成的最后,再fufill promise2, 对应如下代码

newPromise(function(rs,rj){
  rs();
 
}).then(function(data){
    // 对应于x的返回值
    returnnewPromise(rs,rj){
    });
});

4. 若x为一个对象或者函数,如果有then方法,将会执行then方法,then方法this指向x本身,如下

newPromise(function(rs,rj){
 
   rs();
}).then(function(data){
    // 对应于x的返回值
    return{
          a:1,
          then:function(rs,rj){
                console.log(this.a);
                rs({a:2});
          }
    };
}).then(function(data){
   console.log(data.a);
});

5. 如果x没有then方法,那么,x将会做为值来 满足 promise2

newPromise(function(rs,rj){
 
   rs();
}).then(function(data){
    // 对应于x的返回值
    return{
          a:1
    };
}).then(function(data){
   console.log(data.a);
});

Promise A+给出了一些具体的执行细节,保证了then的顺序执行,但在规范中,并未提到Promise.all方法的执行方式

为此,查看bluebird的Promise.all实现方法

BlueBird关于Promise.all实现方法解析

首先,promise中引用promise_array代码如下(已略去一些无关代码)

varINTERNAL=function(){};
varapiRejection=function(msg){
    returnPromise.reject(newTypeError(msg));
};
functionProxyable(){}
 
vartryConvertToPromise=require("./thenables")(Promise,INTERNAL);
varPromiseArray=
    require("./promise_array")(Promise,INTERNAL,
                               tryConvertToPromise,apiRejection,Proxyable);

promise.all的实现也很简单

Promise.all=function(promises){
    returnnewPromiseArray(promises).promise();
};

可见,具体的细节在promise_array中的实现

functionPromiseArray(values){
    varpromise=this._promise=newPromise(INTERNAL);
    if(values instanceofPromise){
        promise._propagateFrom(values,3);
    }
    promise._setOnCancel(this);
 
    this._values=values;
    this._length=0;
    this._totalResolved=0;
 
    // 初始化
    this._init(undefined,-2);
}

PromiseArray的构造方法中,将参数赋值给this._values,待_init方法中使用

PromiseArray.prototype._init=functioninit(_,resolveValueIfEmpty){
    varvalues=tryConvertToPromise(this._values,this._promise);
 
    // 如果values可以转化为promise对象,那么根据不同的状态,会提前return
    if(values instanceofPromise){
        values=values._target();
        varbitField=values._bitField;
        ;
        this._values=values;
 
        // 这个状态是pending的状态
        if(((bitField&50397184)===0)){
            this._promise._setAsyncGuaranteed();
            
            // Promise,将会等Promise对象本身状态改变后再次
            returnvalues._then(
                init,
                this._reject,
                undefined,
                this,
                resolveValueIfEmpty
           );
 
        // FULFILLED, 并没有提前return, 继续进行
        }elseif(((bitField&33554432)!==0)){
            values=values._value();
 
        // rejected的状态,提前终止
        }elseif(((bitField&16777216)!==0)){
            returnthis._reject(values._reason());
        }else{
            returnthis._cancel();
        }
    }
    values=util.asArray(values);
    if(values===null){
        varerr=apiRejection(
            "expecting an array or an iterable object but got "+util.classString(values)).reason();
        this._promise._rejectCallback(err,false);
        return;
    }
 
    if(values.length===0){
        if(resolveValueIfEmpty===-5){
            this._resolveEmptyArray();
        }
        else{
            this._resolve(toResolutionValue(resolveValueIfEmpty));
        }
        return;
    }
    this._iterate(values);
};

init总结为几步

1.尝试转换参数为Promise对象

2.如果转换成功,那么检查Promise对象的状态

  1. Pending,等待Promise

  2. fulfilled, 换取返回值,继续进行

  3. Rejected 终止,返回原因

  4. 其他, 终止

上面的代码可以看出,一旦数组的具有then方法,就可被tryConvertToPromise方法转换为一个Promise对象,如果then方法未实现promise规范,那么Promise对象就会处于Pending的状态,Promise.all方法永远就不会达到fulfilled的条件,问题也就明白了