zoukankan      html  css  js  c++  java
  • 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!
    ' +
                    '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

  • 相关阅读:
    cocospods 卡在 Analyzing dependencies
    android px、sp、dp之间的互转
    Android 4.4环境搭建——Android SDK下载与安装
    我心中的核心组件(可插拔的AOP)~大话开篇及目录
    EF架构~AutoMapper对象映射工具简化了实体赋值的过程
    我心中的核心组件(可插拔的AOP)~第二回 缓存拦截器
    EF架构~为EF DbContext生成的实体添加注释(T5模板应用)
    品味编程~底层开发人员应该这样设计一个字体类
    Study notes for Clustering and K-means
    深入理解Oracle索引(25):一招鲜、吃遍天之单字段索引创建思路
  • 原文地址:https://www.cnblogs.com/web2-developer/p/angular-9.html
Copyright © 2011-2022 走看看