angular源碼分析:angular中$rootscope的實現——scope的一生


在angular中,$scope是一個關鍵的服務,可以被注入到controller中,注入其他服務卻只能是$rootscope。scope是一個概念,是一個類,而$rootscope和被注入到controller中的一個具體的$scope都是一個個具體的對象。$rootscope之所以被稱為"root"的原因就是他是所有scope的祖先,$rootscope是在angular啟動流程中建立的(上上期講過),而被注入到controller中的$scope則是在視圖創建的時候通過父輩的$scope.$new制造出來的,在視圖銷毀的時候,$scope會被跟着銷毀。$scope是鏈接視圖和controller的重要手段,controller則是可以與服務進行鏈接,將服務提供的功能進行組合,然后丟給$scope,$scope則將這些傳遞給視圖,讓它顯示給用戶。
如果將一個$scope比作一個人的話,那么他的功能有:生育功能($new)、進食功能($watch)、消化功能($digest)、執行功能($apply)、交流功能($on、$emit、$broadcast)、死亡功能($destory)。

一、在講$rootscope前,先明確兩個概念:工廠函數和構造函數。

1.工廠函數,能夠根據傳入的參數不同返回同一種性質的對象的函數.

這是一個設計模式級別的概念,不管是面向對象的編程語言還是非面向對象的編程語言,都可以使用這個概念,所謂的工廠模式.感興趣的同學可以自己查閱資料.下面用js舉個例:

function bird_fatory(bird_name,bird_color,bird_fly_way){
   var bird = {};
   bird.name = bird_name;
   bird.color = bird_color;
   bird.fly_way = bird_fly_way;
   return bird;
}

對應到我們將的angular中,factory(name, factoryFn)函數的第二參數要求傳入的就是一個工廠函數,這個工廠返回的就是我們需要的服務.

2.構造函數,能夠通過new運算處理后返回一個對象

這是一個語法級別的概念,需要面向對象的語法特性來支持.就是說,如果我們有一個構造函數Bird,那么就可以通過new Bird()來產出一個Bird類型的對象.
比如:var bird = new Bird()中bird是一個Bird類型的對象,這意味着可以通過instanceof的函數來判斷bird是不是Bird類型:bird instanceof Bird`.
舉例:

function Bird(bird_name,bird_color,bird_fly_way){
   this.name = bird_name;
   this.color = bird_color;
   this.fly_way = bird_fly_way;
//無須返回值
}
var bird = new Bird('dapeng','white','扶搖直上九萬里');

對應到angular中,provider(name, provider_)函數中的第二個參數就要求傳入一個構造函數,並且這個構造函數需要構造一個$get的屬性.

二、$rootscope服務的提供者$rootscopeProvider

下面給出$rootscopeProvider的代碼梗概:

function $RootScopeProvider() {
  var TTL = 10;//TTL是 Time To Live的縮寫,這里是借用的網絡中的一個術語.在我看來,這個是用來控制消耗不良的.
  var $rootScopeMinErr = minErr('$rootScope');
  var lastDirtyWatch = null;
  var applyAsyncId = null;

  this.digestTtl = function(value) {//設置TTL
    if (arguments.length) {
      TTL = value;
    }
    return TTL;
  };

  this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser',
      function($injector, $exceptionHandler, $parse, $browser) {

    function Scope() {//scope的構造函數
      this.$id = nextUid();
      this.$$phase = this.$parent = this.$$watchers =
                     this.$$nextSibling = this.$$prevSibling =
                     this.$$childHead = this.$$childTail = null;
      this.$root = this;
      this.$$destroyed = false;
      this.$$listeners = {};
      this.$$listenerCount = {};
      this.$$watchersCount = 0;
      this.$$isolateBindings = null;
    }

    Scope.prototype = {//scope的原型,相當於他的基因圖譜了吧.
      constructor: Scope
      $new: function(isolate, parent) {...},//一個工廠函數,用於生產一個$scope
      //下面三個函數與watch有關
      $watch: function(watchExp, listener, objectEquality, prettyPrintExpression) {...},
      $watchGroup: function(watchExpressions, listener) {...},
      $watchCollection: function(obj, listener) {...},

      $digest: function() {...},//消化處理

      $destroy: function() {...},//銷毀自己的函數,垃圾回收

      $eval: function(expr, locals) {
        return $parse(expr)(this, locals);
      },
      $evalAsync: function(expr, locals) {...},

      //注冊消化完后要做的事
      $$postDigest: function(fn) {
        postDigestQueue.push(fn);
      },

      $apply: function(expr) {...},
      $applyAsync: function(expr) {...},
      //下面三個函數與事件相關
      $on: function(name, listener) {...},
      $emit: function(name, args) {...},
      $broadcast: function(name, args) {...}
    };

    var $rootScope = new Scope();
    return $rootScope;
  };
}

三、$scope的一生

$scope是一個對象,那么就是一個變量,變量都是有生命周期的.從生到死,需要的時候被創造出來,不需要的時候被消滅.但失去存在意思的時候還不離開,就會擠占其他生命的空間,表現在系統層面上就是內存泄漏.

1.$scope的出生,$new

第一代的Scope是天生的,這個天就是$rootscopeProvider,其他的scope都是$scope.$new生出來的.來看看她的代碼吧:

 function createChildScopeClass(parent) {
    function ChildScope() {
      this.$$watchers = this.$$nextSibling =
          this.$$childHead = this.$$childTail = null;
      this.$$listeners = {};
      this.$$listenerCount = {};
      this.$$watchersCount = 0;
      this.$id = nextUid();//生成一個唯一的id
      this.$$ChildScope = null;
    }
    ChildScope.prototype = parent;//將原型設為他媽
    return ChildScope;
  }

 function(isolate, parent) {
        var child;

        parent = parent || this;

        if (isolate) { //孤立作用域時
          child = new Scope();//利用構造函數構造一個scope
          child.$root = this.$root;//和老祖宗取得聯系
        } else {
          // Only create a child scope class if somebody asks for one,
          // but cache it to allow the VM to optimize lookups.
          if (!this.$$ChildScope) {
            this.$$ChildScope = createChildScopeClass(this);//上面定義了這個函數
          }
          child = new this.$$ChildScope();//生產一個孩子
        }
        child.$parent = parent;//和他媽建立聯系
        //孩子多了,要排個大小先后
        child.$$prevSibling = parent.$$childTail;
        if (parent.$$childHead) {
          parent.$$childTail.$$nextSibling = child;
          parent.$$childTail = child;
        } else {
          parent.$$childHead = parent.$$childTail = child;
        }

        // When the new scope is not isolated or we inherit from `this`, and
        // the parent scope is destroyed, the property `$$destroyed` is inherited
        // prototypically. In all other cases, this property needs to be set
        // when the parent scope is destroyed.
        // The listener needs to be added after the parent is set
        if (isolate || parent != this) child.$on('$destroy', destroyChildScope);//監聽到消耗事件,銷毀自己一家子

        return child;
      }
``
函數需要傳入兩個參數,第一個參數表示該scope是否是孤立的(就是能不能訪問他媽媽的屬性),第二參數就是孩子她媽是誰.

###2.我將$scope.$watch理解為吃東西.說是看看,其實是吃到肚子里去了.
```js
$watch: function(watchExp, listener, objectEquality, prettyPrintExpression) {
        var get = $parse(watchExp);//$parse將一個表達式轉換為一個函數

        if (get.$$watchDelegate) {//這里提供了一個代理功能,可以思考下怎么用哦
          return get.$$watchDelegate(this, listener, objectEquality, get, watchExp);
        }
        var scope = this,
            array = scope.$$watchers,//用於存儲watcher對象
            watcher = { //watcher對象
              fn: listener,
              last: initWatchVal,
              get: get,
              exp: prettyPrintExpression || watchExp,
              eq: !!objectEquality
            };

        lastDirtyWatch = null;

        if (!isFunction(listener)) {
          watcher.fn = noop;
        }

        if (!array) {
          array = scope.$$watchers = [];
        }
        // we use unshift since we use a while loop in $digest for speed.
        // the while loop reads in reverse order.
        array.unshift(watcher);
        incrementWatchersCount(this, 1);

        return function deregisterWatch() {//返回的是一個函數,這個函數可以用於銷毀注冊的watch
          if (arrayRemove(array, watcher) >= 0) {
            incrementWatchersCount(scope, -1);
          }
          lastDirtyWatch = null;
        };
      },

這個函數的功能是注冊一個監聽,監聽的內容是一個表達式,監聽者是一個函數.函數通過將表達式和監聽者組裝成一個wacher,放到一個數組scope.$$watchers中.換一個方式,可以理解成他在吃東西,要吃的東西是叫監聽對(監聽表達式和監聽者),吃到胃里.這個胃就是scope.$$watchers.
另外還有兩個函數:$watchGroup,用於監聽一個表達式數組;$watchCollection,用於監聽scope上所有屬性的變化.他們最終都會往scope.$$watchers上加入元素.

3.吃了,總得消化:$digest,以及消化完后做的啥$$postDigest

3.1$$postDigest:用於注冊在消化后要執行的函數.

3.2消化的源代碼:

$digest: function() {
        var watch, value, last,
            watchers,
            length,
            dirty, ttl = TTL,
            next, current, target = this,
            watchLog = [],
            logIdx, logMsg, asyncTask;

        beginPhase('$digest');
        // Check for changes to browser url that happened in sync before the call to $digest
        $browser.$$checkUrlChange();//檢測url地址是否發生變化

        if (this === $rootScope && applyAsyncId !== null) {
          // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then
          // cancel the scheduled $apply and flush the queue of expressions to be evaluated.
          $browser.defer.cancel(applyAsyncId);
          flushApplyAsync();
        }

        lastDirtyWatch = null;

        do { // "while dirty" loop  //一輪消化的過程就是將asyncQueue隊列中的函數執行完 和將吃到肚子的監聽任務依次檢查一篇.
          dirty = false;
          current = target;

          while (asyncQueue.length) {//執行異步隊列中的函數
            try {
              asyncTask = asyncQueue.shift();
              asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals);
            } catch (e) {
              $exceptionHandler(e);
            }
            lastDirtyWatch = null;
          }

          traverseScopesLoop:
          do { // "traverse the scopes" loop  //檢查監聽,如果滿足監聽的條件,執行監聽者函數
            if ((watchers = current.$$watchers)) {
              // process our watches
              length = watchers.length;
              while (length--) {
                try {
                  watch = watchers[length];
                  // Most common watches are on primitives, in which case we can short
                  // circuit it with === operator, only when === fails do we use .equals
                  if (watch) {
                    if ((value = watch.get(current)) !== (last = watch.last) &&
                        !(watch.eq
                            ? equals(value, last)
                            : (typeof value === 'number' && typeof last === 'number'
                               && isNaN(value) && isNaN(last)))) {
                      dirty = true;
                      lastDirtyWatch = watch;
                      watch.last = watch.eq ? copy(value, null) : value;
                      watch.fn(value, ((last === initWatchVal) ? value : last), current);
                      if (ttl < 5) {
                        logIdx = 4 - ttl;
                        if (!watchLog[logIdx]) watchLog[logIdx] = [];
                        watchLog[logIdx].push({
                          msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp,
                          newVal: value,
                          oldVal: last
                        });
                      }
                    } else if (watch === lastDirtyWatch) {
                      // If the most recently dirty watcher is now clean, short circuit since the remaining watchers
                      // have already been tested.
                      dirty = false;
                      break traverseScopesLoop;
                    }
                  }
                } catch (e) {
                  $exceptionHandler(e);
                }
              }
            }

            // Insanity Warning: scope depth-first traversal
            // yes, this code is a bit crazy, but it works and we have tests to prove it!
            // this piece should be kept in sync with the traversal in $broadcast
            if (!(next = ((current.$$watchersCount && current.$$childHead) ||
                (current !== target && current.$$nextSibling)))) {
              while (current !== target && !(next = current.$$nextSibling)) {
                current = current.$parent;
              }
            }
          } while ((current = next));

          // `break traverseScopesLoop;` takes us to here

          if ((dirty || asyncQueue.length) && !(ttl--)) {//控制消耗的次數,操作了一定限度,拋出異常,這個ttl可以在Provider中設置:digestTtl
            clearPhase();
            throw $rootScopeMinErr('infdig',
                '{0} $digest() iterations reached. Aborting!\n' +
                'Watchers fired in the last 5 iterations: {1}',
                TTL, watchLog);
          }

        } while (dirty || asyncQueue.length);

        clearPhase();

        while (postDigestQueue.length) {//執行消化完后的函數列表.
          try {
            postDigestQueue.shift()();
          } catch (e) {
            $exceptionHandler(e);
          }
        }
      },

消化的過程,實際是一個執行異步隊列中的函數和檢測監聽的過程.但是在這個過程中,很可能在異步隊列中產出新的函數,以及讓監聽滿足條件(dirty),那么需要再次消化.但是消化不能沒完沒了,不然就會占用所用的執行時間,線程會被阻塞在這里(也就是消化不良勒).所以需要有一個最大次數,也就是TTL,這個TTL允許在angular的配置階段進行修改.

3.3.消化是怎么開始的呢?

angular是事件驅動的,當試圖上發生任何事件或者定時器事件到時執行時,消化過程都會被啟動.

4.吃飽了還得干點其他事:$apply,$applyAsync,$eval,$evalAsync

4.1.$eval,馬上執行某個表達式

4.2.$evalAsync,異步執行某個表達式.

$evalAsync: function(expr, locals) {
        // if we are outside of an $digest loop and this is the first time we are scheduling async
        // task also schedule async auto-flush
        if (!$rootScope.$$phase && !asyncQueue.length) {
          $browser.defer(function() {//異步喚起消化過程,關於$browser,后面會講
            if (asyncQueue.length) {
              $rootScope.$digest();
            }
          });
        }

        asyncQueue.push({scope: this, expression: expr, locals: locals});//這個函數是通過向異步隊列中添加對象來完成的.
      },

4.3.$apply,先執行,后喚起消化過程.

      $apply: function(expr) {
        try {
          beginPhase('$apply');
          try {
            return this.$eval(expr);
          } finally {
            clearPhase();
          }
        } catch (e) {
          $exceptionHandler(e);
        } finally {
          try {   //無論如何都嘗試喚起消化過程
            $rootScope.$digest();
          } catch (e) {
            $exceptionHandler(e);
            throw e;
          }
        }
      },

4.4$applyAsync,喚起消化過程來完成 表達式的執行

$applyAsync: function(expr) {
        var scope = this;
        expr && applyAsyncQueue.push($applyAsyncExpression);
        scheduleApplyAsync();

        function $applyAsyncExpression() {
          scope.$eval(expr);
        }
      },

5.$scope間的交流:$on,$emit,$broadcast

5.1.$on,注冊事件監聽.就是當由某事件發生(來自父輩或者子輩),就去執行某個函數.

      $on: function(name, listener) {
        var namedListeners = this.$$listeners[name];
        if (!namedListeners) {
          this.$$listeners[name] = namedListeners = [];
        }
        namedListeners.push(listener);

        var current = this;
        do {
          if (!current.$$listenerCount[name]) {
            current.$$listenerCount[name] = 0;
          }
          current.$$listenerCount[name]++;
        } while ((current = current.$parent));

        var self = this;
        return function() {
          var indexOfListener = namedListeners.indexOf(listener);
          if (indexOfListener !== -1) {
            namedListeners[indexOfListener] = null;
            decrementListenerCount(self, 1, name);
          }
        };
      },

5.2.$emit,告訴父輩們,某件事發生了.事件會一直抵達$rootscope

$emit: function(name, args) {
        var empty = [],
            namedListeners,
            scope = this,
            stopPropagation = false,
            event = {
              name: name,
              targetScope: scope,
              stopPropagation: function() {stopPropagation = true;},
              preventDefault: function() {
                event.defaultPrevented = true;
              },
              defaultPrevented: false
            },
            listenerArgs = concat([event], arguments, 1),
            i, length;

        do {
          namedListeners = scope.$$listeners[name] || empty;
          event.currentScope = scope;
          for (i = 0, length = namedListeners.length; i < length; i++) {

            // if listeners were deregistered, defragment the array
            if (!namedListeners[i]) {
              namedListeners.splice(i, 1);
              i--;
              length--;
              continue;
            }
            try {
              //allow all listeners attached to the current scope to run
              namedListeners[i].apply(null, listenerArgs);
            } catch (e) {
              $exceptionHandler(e);
            }
          }
          //if any listener on the current scope stops propagation, prevent bubbling
          if (stopPropagation) {
            event.currentScope = null;
            return event;
          }
          //traverse upwards
          scope = scope.$parent;
        } while (scope);

        event.currentScope = null;

        return event;
      },

5.3.$broadcast,告訴子輩們,某事發生了.事件會傳遞給所有子輩scope,所以叫廣播.

$broadcast: function(name, args) {
        var target = this,
            current = target,
            next = target,
            event = {
              name: name,
              targetScope: target,
              preventDefault: function() {
                event.defaultPrevented = true;
              },
              defaultPrevented: false
            };

        if (!target.$$listenerCount[name]) return event;

        var listenerArgs = concat([event], arguments, 1),
            listeners, i, length;

        //down while you can, then up and next sibling or up and next sibling until back at root
        while ((current = next)) {
          event.currentScope = current;
          listeners = current.$$listeners[name] || [];
          for (i = 0, length = listeners.length; i < length; i++) {
            // if listeners were deregistered, defragment the array
            if (!listeners[i]) {
              listeners.splice(i, 1);
              i--;
              length--;
              continue;
            }

            try {
              listeners[i].apply(null, listenerArgs);
            } catch (e) {
              $exceptionHandler(e);
            }
          }

          // Insanity Warning: scope depth-first traversal
          // yes, this code is a bit crazy, but it works and we have tests to prove it!
          // this piece should be kept in sync with the traversal in $digest
          // (though it differs due to having the extra check for $$listenerCount)
          if (!(next = ((current.$$listenerCount[name] && current.$$childHead) ||
              (current !== target && current.$$nextSibling)))) {
            while (current !== target && !(next = current.$$nextSibling)) {
              current = current.$parent;
            }
          }
        }

        event.currentScope = null;
        return event;
      }
    };

6.老而不死謂之賊:$destory

對於生死,前面說了.這里不再贅述.上代碼:

$destroy: function() {
        // We can't destroy a scope that has been already destroyed.
        if (this.$$destroyed) return;
        var parent = this.$parent;

        this.$broadcast('$destroy');//自己死的時候,會把這個消息傳遞給子代,子代也就死了.沒辦法,自己死后,沒人管得了他們了.所以一起帶走.
        this.$$destroyed = true;

        if (this === $rootScope) {
          //Remove handlers attached to window when $rootScope is removed
          $browser.$$applicationDestroyed();
        }

        incrementWatchersCount(this, -this.$$watchersCount);
        for (var eventName in this.$$listenerCount) {
          decrementListenerCount(this, this.$$listenerCount[eventName], eventName);
        }

        // sever all the references to parent scopes (after this cleanup, the current scope should
        // not be retained by any of our references and should be eligible for garbage collection)
        if (parent && parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
        if (parent && parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
        if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
        if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;

        // Disable listeners, watchers and apply/digest methods
        this.$destroy = this.$digest = this.$apply = this.$evalAsync = this.$applyAsync = noop;
        this.$on = this.$watch = this.$watchGroup = function() { return noop; };
        this.$$listeners = {};

        // All of the code below is bogus code that works around V8's memory leak via optimized code
        // and inline caches.
        //
        // see:
        // - https://code.google.com/p/v8/issues/detail?id=2073#c26
        // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909
        // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451

        this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead =
            this.$$childTail = this.$root = this.$$watchers = null;
      },

上一期:angular源碼分析:圖解angular的啟動流程
下一期:angular源碼分析:angular中臟活累活承擔者之$parse


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM