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

@kuitos 2015-08-23 09:41:12发表于 kuitos/kuitos.github.io Angularjavascript

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用法,敬请期待