kuitos / kuitos.github.io

📝Kuitos's Blog https://github.com/kuitos/kuitos.github.io/issues
https://kuitos.github.io/
MIT License
1.13k stars 81 forks source link

Javascript异步流程控制之Promise(2)-Angular $q源码解读 #16

Open kuitos opened 9 years ago

kuitos commented 9 years ago

Javascript异步流程控制之Promise(2)-Angular $q源码解读

原文写于 2015-01-27

接上一篇 Angular $q简介 ,这一篇我们就Angular对$q的代码实现来学习下Promise的实现原理及思想
上篇讲到了Angular Promise的基本API,其中就典型的应用类似这样

Promise.then

var defer = $q.defer();

setTimeout(function (){
    defer.resolve(10);
},5000);
console.log("1");
defer.promise.then(function(a){
    console.log(a);
});
console.log("2");

// 打印的顺序是1,2,10
// 当我们依赖的数据会在一个异步的时间点返回时,我们需要使用resolve 跟 then 来配合

首先我们要明确一个概念就是js是单线程语言,一个时间只能有一个线程在执行。那么上面的执行流程自然就是: 启动定时器并往事件队列里加入一个回调函数 ---> 打印1 ---> 调用defer.promise.then方法 ---> 打印2 --->主线程空闲开始等5秒(实际会小于5s)执行回调 ---> 调用defer.resolve ---> 打印10
至于这中间究竟发生了上面,先来看看angular是怎样实现这两个方法的

  1. Promise.then

    // Promise.then
    function Promise() {
    this.$$state = { status: 0 };
    }
    
    Promise.prototype = {
       then: function(onFulfilled, onRejected, progressBack) {
           var result = new Deferred();
    
           this.$$state.pending = this.$$state.pending || [];
           this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]);
           if (this.$$state.status > 0) scheduleProcessQueue(this.$$state);
    
           return result.promise;
       },
    .........

    Promise构造函数原型上维护一个方法 then ,当一个promise对象调用该方法时会往自己的成员属性 $$state 上构建一个 pending数组,然后将生成的deferred跟成功、失败回调压入pending数组。
    拿上文的案例来看, defer.promise.then之后 defer.promise.$$state.pending = [[new Deferred(), function (a){console.log(a)}]];
    再来看看defer.resolve发生了什么

  2. Deferred.resolve

    // Deferred.resolve
    Deferred.prototype = {
       resolve: function(val) {
           if (this.promise.$$state.status) return;
           if (val === this.promise) {
               this.$$reject($qMinErr(
                   'qcycle',
               "Expected promise to be resolved with value other than itself '{0}'",
               val));
           }
           else {
               this.$$resolve(val);
           }
    
       },
    
       $$resolve: function(val) {
           var then, fns;
    
           fns = callOnce(this, this.$$resolve, this.$$reject);
           try {
               if ((isObject(val) || isFunction(val))) then = val && val.then;
               if (isFunction(then)) {
                   this.promise.$$state.status = -1;
                   then.call(val, fns[0], fns[1], this.notify);
               } else {
                   this.promise.$$state.value = val;
                   this.promise.$$state.status = 1;
                   scheduleProcessQueue(this.promise.$$state);
               }
           } catch (e) {
               fns[1](e);
               exceptionHandler(e);
           }
       },
    ......

    核心方法是 Deferred.$$resolve , 上文中 defer.resolve(10) 之后会走到 scheduleProcessQueue(this.promise.$$state);

    // scheduleProcessQueue
    function scheduleProcessQueue(state) {
     if (state.processScheduled || !state.pending) return;
     state.processScheduled = true;
     nextTick(function() { processQueue(state); });
    }

    nextTick方法定义在这里

    // nextTick
    function qFactory(nextTick, exceptionHandler) {
     var $qMinErr = minErr('$q', TypeError);
     .....

    再往上看谁调用了qFactory

    // qFactory
    function $$QProvider() {
     this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) {
       return qFactory(function(callback) {
         $browser.defer(callback);
       }, $exceptionHandler);
     }];
    }

    没错就是我们使用的 $q 服务,也就是$q在初始化的时候会 生成 nextTick = function(callback) {$browser.defer(callback);}
    $browser.defer干了什么事?

    // $browser.defer
    self.defer = function(fn, delay) {
     var timeoutId;
     outstandingRequestCount++;
     timeoutId = setTimeout(function() {
       delete pendingDeferIds[timeoutId];
       completeOutstandingRequest(fn);
     }, delay || 0);
     pendingDeferIds[timeoutId] = true;
     return timeoutId;
    };

    没错,很简单,就是一个定时器。将定时器时间默认设置为0的意图很明显,使当前任务脱离主线程,在事件队列执行时再做处理,也就是所谓的defer延时
    回到 scheduleProcessQueue,被手动从主线程中移除的任务是 nextTick(function() { processQueue(state); });
    看看 processQueue 是干嘛的

    // processQueue
    function processQueue(state) {
     var fn, promise, pending;
    
     pending = state.pending;
     state.processScheduled = false;
     state.pending = undefined;
     for (var i = 0, ii = pending.length; i < ii; ++i) {
       promise = pending[i][0];
       fn = pending[i][state.status];
       try {
         if (isFunction(fn)) {
           promise.resolve(fn(state.value));
         } else if (state.status === 1) {
           promise.resolve(state.value);
         } else {
           promise.reject(state.value);
         }
       } catch (e) {
         promise.reject(e);
         exceptionHandler(e);
       }
     }
    }

    没错,这个就是Promise处理最核心的部位。当主线程执行完毕开始处理事件队列时,processQueue 开始执行, 它会将 state.pending 队列(没错,队列里的内容就是我们调用then()方法时一个个压入的)遍历然后依次调用
    假如有这样一段代码
    promiseO.then(funcA).then(funcB);
    那么执行后会发生这样一个变化

    promiseO.then(funcA) ---> promiseO.$$state.pengding = [[deferredA, funcA]]; return deferredA.promise;  
    deferredA.promise.then(funcB) ---> deferredA.promise.$$state.pending = [[deferredB, funcB]]; return deferredB.promise;  
    
    // 当各个promise被依次resolve了
    // 1. 首先是promiseO.resolve(10) ---> promiseO.$$state.value = 10;promiseO.$$state.status = 1;scheduleProcessQueue(promiseO.$$state);
    // 2. scheduleProcessQueue最后走到processQueue方法,会执行promiseO.$$state.pengding队列,这时候会执行 deferredA.resolve(funcA(promiseO.$$state.value))
    // 3. 假设funcA(promiseO.$$state.value)返回20,那么就是 deferredA.resolve(20)。这时候就又回到了步骤1
    // 依次执行后then链上所有方法均会执行

    then的核心思想是它会往当前promise的pending队列中压入then链下一个promise对象的Deferred(promise=Deferred.promise),然后通过这种节点关系构成整个then链

    介绍完promise.then我们再来介绍一下$q.all([promises]).then(funcT),来看看他是怎么实现当所有promise被resolve之后再走向下一步的吧

  3. $q.all

    // $q.all
    function all(promises) {
     var deferred = new Deferred(),
         counter = 0,
         results = isArray(promises) ? [] : {};
    
     forEach(promises, function(promise, key) {
       counter++;
       when(promise).then(function(value) {
         if (results.hasOwnProperty(key)) return;
         results[key] = value;
         if (!(--counter)) deferred.resolve(results);
       }, function(reason) {
         if (results.hasOwnProperty(key)) return;
         deferred.reject(reason);
       });
     });
    
     if (counter === 0) {
       deferred.resolve(results);
     }
    
     return deferred.promise;
    }

    很简单,forEach promises列表之后,在每个promise的then链加一个回调,关键代码在这一行
    if (!(--counter)) deferred.resolve(results);
    每一个promise成功执行了then回调之后会--counter,如果某个promise是最后一个被resolve的则将所有的results resolve,这个时候他的then链就开始执行了

  4. $q.when

    //$q.when
    var when = function(value, callback, errback, progressBack) {
     var result = new Deferred();
     result.resolve(value);
     return result.promise.then(callback, errback, progressBack);
    };

    $q.when(val/promiseLikeObj)的用处很简单,就是将一个简单对象/或promiseLike的对象包装成$q信任的promise对象并将值加入到then链中。

    写在最后:

    至此$q的几个基本api都基于源码做了简单介绍,个人在研究源码的过程中最大的收获就是对js的异步模型有了更深刻的理解,并对工程化应用有了感官的认识,强烈建议有兴趣的同学读读源码。Promise是一种异步编程模型,是当前被普遍认可的一种解决方案,目前业界还有一些其他的异步解决方案诸如 thunk、co 模型等,思路各有差异跟优缺点,有兴趣的同学可以去了解相信会对你理解js的异步模型有进一步认识。

PS:
下一篇会介绍一下ES6中的Promise的api用法,敬请期待