• AngularJS 3


    AngularJS 源码分析3

    本文接着上一篇讲

     

    上一篇地址

     


     

    回顾

     

    上次说到了rootScope里的$watch方法中的解析监控表达式,即而引出了对parse的分析,今天我们接着这里继续挖代码.

     

    $watch续

     

    先上一块$watch代码

     

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    $watch: function(watchExp, listener, objectEquality) {
            var scope = this,
                get = compileToFn(watchExp, 'watch'),
                array = scope.$$watchers,
                watcher = {
                  fn: listener,
                  last: initWatchVal,
                  get: get,
                  exp: watchExp,
                  eq: !!objectEquality
                };
     
            lastDirtyWatch = null;
     
            // in the case user pass string, we need to compile it, do we really need this ?
            if (!isFunction(listener)) {
              var listenFn = compileToFn(listener || noop, 'listener');
              watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);};
            }
     
            if (typeof watchExp == 'string' && get.constant) {
              var originalFn = watcher.fn;
              watcher.fn = function(newVal, oldVal, scope) {
                originalFn.call(this, newVal, oldVal, scope);
                arrayRemove(array, watcher);
              };
            }
     
            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);
     
            return function deregisterWatch() {
              arrayRemove(array, watcher);
              lastDirtyWatch = null;
            };
          }

     

     

    这里的get = compileToFn(watchExp, 'watch'),上篇已经分析完了,这里返回的是一个执行表达式的函数,接着往下看,这里初始化了一个watcher对象,用来保存一些监听相关的信息,简单的说明一下

     

    • fn, 代表监听函数,当监控表达式新旧不相等时会执行此函数
    • last, 保存最后一次发生变化的监控表达式的值
    • get, 保存一个监控表达式对应的函数,目的是用来获取表达式的值然后用来进行新旧对比的
    • exp, 保存一个原始的监控表达式
    • eq, 保存$watch函数的第三个参数,表示是否进行深度比较

     

    然后会检查传递进来的监听参数是否为函数,如果是一个有效的字符串,则通过parse来解析生成一个函数,否则赋值为一个noop占位函数,最后生成一个包装函数,函数体的内容就是执行刚才生成的监听函数,默认传递当前作用域.

     

    接着会检查监控表达式是否为字符串并且执行表达式的constant为true,代表这个字符串是一个常量,那么,系统在处理这种监听的时候,执行完一次监听函数之后就会删除这个$watch.最后往当前作用域里的$$watchers数组头中添加$watch信息,注意这里的返回值,利用JS的闭包保留了当前的watcher,然后返回一个函数,这个就是用来删除监听用的.

     

    $eval

     

    这个$eval也是挺方便的函数,假如你想直接在程序里执行一个字符串的话,那么可以这么用

     

     

    1
    2
    $scope.name = '2';
    $scope.$eval('1+name'); // ==> 会输出12

     

     

    大家来看看它的函数体

     

     

    1
    return $parse(expr)(this, locals);

     

     

    其实就是通过parse来解析成一个执行表达式函数,然后传递当前作用域以及额外的参数,返回这个执行表达式函数的值

     

    $evalAsync

     

    evalAsync函数的作用就是延迟执行表达式,并且执行完不管是否异常,触发dirty check.

     

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) {
              $browser.defer(function() {
                if ($rootScope.$$asyncQueue.length) {
                  $rootScope.$digest();
                }
              });
            }
     
    this.$$asyncQueue.push({scope: this, expression: expr});

     

     

    可以看到当前作用域内部有一个$$asyncQueue异步队列,保存着所有需要延迟执行的表达式,此处的表达式可以是字符串或者函数,因为这个表达式最终会调用$eval方法,注意这里调用了$browser服务的defer方法,从ng->browser.js源码里可以看到,其实这里就是调用setTimeout来实现的.

     

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    self.defer = function(fn, delay) {
        var timeoutId;
        outstandingRequestCount++;
        timeoutId = setTimeout(function() {
          delete pendingDeferIds[timeoutId];
          completeOutstandingRequest(fn);
        }, delay || 0);
        pendingDeferIds[timeoutId] = true;
        return timeoutId;
      };

     

     

    上面的代码主要是延迟执行函数,另外pendingDeferIds对象保存所有setTimeout返回的id,这个会在self.defer.cancel这里可以取消执行延迟执行.

     

    说digest方法之前,还有一个方法要说说

     

    $postDigest

     

    这个方法跟evalAsync不同的时,它不会主动触发digest方法,只是往postDigestQueue队列中增加执行表达式,它会在digest体内最后执行,相当于在触发dirty check之后,可以执行别的一些逻辑.

     

     

    1
    this.$$postDigestQueue.push(fn);

     

     

    下面我们来重点说说digest方法

     

    $digest

     

    digest方法是dirty check的核心,主要思路是先执行$$asyncQueue队列中的表达式,然后开启一个loop来的执行所有的watch里的监听函数,前提是前后两次的值是否不相等,假如ttl超过系统默认值,则dirth check结束,最后执行$$postDigestQueue队列里的表达式.

     

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    $digest: function() {
            var watch, value, last,
                watchers,
                asyncQueue = this.$$asyncQueue,
                postDigestQueue = this.$$postDigestQueue,
                length,
                dirty, ttl = TTL,
                next, current, target = this,
                watchLog = [],
                logIdx, logMsg, asyncTask;
     
            beginPhase('$digest');
     
            lastDirtyWatch = null;
     
            do { // "while dirty" loop
              dirty = false;
              current = target;
     
              while(asyncQueue.length) {
                try {
                  asyncTask = asyncQueue.shift();
                  asyncTask.scope.$eval(asyncTask.expression);
                } catch (e) {
                  clearPhase();
                  $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) : value;
                          watch.fn(value, ((last === initWatchVal) ? value : last), current);
                          if (ttl < 5) {
                            logIdx = 4 - ttl;
                            if (!watchLog[logIdx]) watchLog[logIdx] = [];
                            logMsg = (isFunction(watch.exp))
                                ? 'fn: ' + (watch.exp.name || watch.exp.toString())
                                : watch.exp;
                            logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last);
                            watchLog[logIdx].push(logMsg);
                          }
                        } 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) {
                      clearPhase();
                      $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.$$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--)) {
                clearPhase();
                throw $rootScopeMinErr('infdig',
                    '{0} $digest() iterations reached. Aborting! ' +
                    'Watchers fired in the last 5 iterations: {1}',
                    TTL, toJson(watchLog));
              }
     
            } while (dirty || asyncQueue.length);
     
            clearPhase();
     
            while(postDigestQueue.length) {
              try {
                postDigestQueue.shift()();
              } catch (e) {
                $exceptionHandler(e);
              }
            }
          }

     

     

    通过上面的代码,可以看出,核心就是两个loop,外loop保证所有的model都能检测到,内loop则是真实的检测每个watch,watch.get就是计算监控表达式的值,这个用来跟旧值进行对比,假如不相等,则执行监听函数

     

    注意这里的watch.eq这是是否深度检查的标识,equals方法是angular.js里的公共方法,用来深度对比两个对象,这里的不相等有一个例外,那就是NaN ===NaN,因为这个永远都是false,所以这里加了检查

     

     

    1
    2
    3
    4
    !(watch.eq
        ? equals(value, last)
        : (typeof value == 'number' && typeof last == 'number'
           && isNaN(value) && isNaN(last)))

     

     

    比较完之后,把新值传给watch.last,然后执行watch.fn也就是监听函数,传递三个参数,分别是:最新计算的值,上次计算的值(假如是第一次的话,则传递新值),最后一个参数是当前作用域实例,这里有一个设置外loop的条件值,那就是dirty = true,也就是说只要内loop执行了一次watch,则外loop还要接着执行,这是为了保证所有的model都能监测一次,虽然这个有点浪费性能,不过超过ttl设置的值后,dirty check会强制关闭,并抛出异常

     

     

    1
    2
    3
    4
    5
    6
    7
    if((dirty || asyncQueue.length) && !(ttl--)) {
        clearPhase();
        throw $rootScopeMinErr('infdig',
            '{0} $digest() iterations reached. Aborting! ' +
            'Watchers fired in the last 5 iterations: {1}',
            TTL, toJson(watchLog));
    }

     

     

    这里的watchLog日志对象是在内loop里,当ttl低于5的时候开始记录的

     

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    if (ttl < 5) {
        logIdx = 4 - ttl;
        if (!watchLog[logIdx]) watchLog[logIdx] = [];
        logMsg = (isFunction(watch.exp))
            ? 'fn: ' + (watch.exp.name || watch.exp.toString())
            : watch.exp;
        logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last);
        watchLog[logIdx].push(logMsg);
    }

     

     

    当检查完一个作用域内的所有watch之后,则开始深度遍历当前作用域的子级或者父级,虽然这有些影响性能,就像这里的注释写的那样yes, this code is a bit crazy

     

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 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.$$childHead ||
          (current !== target && current.$$nextSibling)))) {
        while(current !== target && !(next = current.$$nextSibling)) {
          current = current.$parent;
        }
    }

     

     

    上面的代码其实就是不断的查找当前作用域的子级,没有子级,则开始查找兄弟节点,最后查找它的父级节点,是一个深度遍历查找.只要next有值,则内loop则一直执行

     

     

    1
    while ((current = next))

     

     

    不过内loop也有跳出的情况,那就是当前watch跟最后一次检查的watch相等时就退出内loop.

     

     

    1
    2
    3
    4
    5
    6
    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;
    }

     

     

    注意这个内loop同时也是一个label(标签)语句,这个可以在loop中执行跳出操作就像上面的break

     

    正常执行完两个loop之后,清除当前的阶段标识clearPhase();,然后开始执行postDigestQueue队列里的表达式.

     

     

    1
    2
    3
    4
    5
    6
    7
    while(postDigestQueue.length) {
        try {
          postDigestQueue.shift()();
        } catch (e) {
          $exceptionHandler(e);
        }
    }

     

     

    接下来说说,用的也比较多的$apply方法

     

    $apply

     

    这个方法一般用在,不在ng的上下文中执行js代码的情况,比如原生的DOM事件中执行想改变ng中某些model的值,这个时候就要使用$apply方法了

     

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    $apply: function(expr) {
        try {
          beginPhase('$apply');
          return this.$eval(expr);
        } catch (e) {
          $exceptionHandler(e);
        } finally {
          clearPhase();
          try {
            $rootScope.$digest();
          } catch (e) {
            $exceptionHandler(e);
            throw e;
          }
        }
    }

     

     

    代码中,首先让当前阶段标识为$apply,这个可以防止使用$apply方法时检查是否已经在这个阶段了,然后就是执行$eval方法, 这个方法上面有讲到,最后执行$digest方法,来使ng中的M或者VM改变.

     

    接下来说说scope中event模块,它的api跟一般的event事件模块比较像,提供有$on,$emit,$broadcast,这三个很实用的方法

     

    $on

     

    这个方法是用来定义事件的,这里用到了两个实例变量$$listeners, $$listenerCount,分别用来保存事件,以及事件数量计数

     

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    $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() {
              namedListeners[indexOf(namedListeners, listener)] = null;
              decrementListenerCount(self, 1, name);
            };
          }

     

     

    分析上面的代码,可以看出每当定义一个事件的时候,都会向$$listeners对象中添加以name为key的属性,值就是事件执行函数,注意这里有个事件计数,只要有父级,则也给父级的$$listenerCount添加以name为key的属性,并且值+1,这个$$listenerCount 会在广播事件的时候用到,最后这个方法返回一个取消事件的函数,先设置$$listeners中以name为key的值为null,然后调用decrementListenerCount来使该事件计数-1.

     

    $emit

     

    这个方法是用来触发$on定义的事件,原理就是loop$$listeners属性,检查是否有值,有的话,则执行,然后依次往上检查父级,这个方法有点类似冒泡执行事件.

     

    $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) return event;
          //traverse upwards
          scope = scope.$parent;
        } while (scope);
    
        return event;
      }
    

     

    上面的代码比较简单,首先定义一个事件参数,然后开启一个loop,只要scope有值,则一直执行,这个方法的事件链是一直向上传递的,不过当在事件函数执行stopPropagation方法,就会停止向上传递事件.

     

    $broadcast

     

    这个是$emit的升级版,广播事件,即能向上传递,也能向下传递,还能平级传递,核心原理就是利用深度遍历当前作用域

     

    $broadcast: function(name, args) { var target = this, current = target, next = target, event = { name: name, targetScope: target, preventDefault: function() { event.defaultPrevented = true; }, defaultPrevented: false }, 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;
        }
      }
    }
    
    return event;
    

     

    }

     

    代码跟$emit差不多,只是跟它不同的时,这个是不断的取next值,而next的值则是通过深度遍历它的子级节点,兄弟节点,父级节点,依次查找可用的以name为key的事件.注意这里的注释,跟$digest里的差不多,都是通过深度遍历查找,所以$broadcast方法也不能常用,性能不是很理想

     

    $destroy

     

    这个方法是用来销毁当前作用域,代码主要是清空当前作用域内的一些实例属性,以免执行digest,$emit,$broadcast时会关联到

     

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    $destroy: function() {
        // we can't destroy the root scope or a scope that has been already destroyed
        if (this.$$destroyed) return;
        var parent = this.$parent;
     
        this.$broadcast('$destroy');
        this.$$destroyed = true;
        if (this === $rootScope) return;
     
        forEach(this.$$listenerCount, bind(null, decrementListenerCount, this));
     
        // 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.$$childHead == this) parent.$$childHead = this.$$nextSibling;
        if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
        if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
        if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
     
     
        // All of the code below is bogus code that works around V8's memory leak via optimized code
        // and inline caches.
        //
        // see:
        // - 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 = null;
     
        // don't reset these to null in case some async task tries to register a listener/watch/task
        this.$$listeners = {};
        this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = [];
     
        // prevent NPEs since these methods have references to properties we nulled out
        this.$destroy = this.$digest = this.$apply = noop;
        this.$on = this.$watch = function() { return noop; };
    }

     

     

    代码比较简单,先是通过foreach来清空$$listenerCount实例属性,然后再设置$parent,$$nextSibling,$$prevSibling,$$childHead,$$childTail,$root为null,清空$$listeners,$$watchers,$$asyncQueue,$$postDigestQueue,最后就是重罢方法为noop占位函数

     

    总结

     

    rootScope说完了,这是个使用比例非常高的核心provider,分析的比较简单,有啥错误的地方,希望大家能够指出来,大家一起学习学习,下次有空接着分析别的.

     


     

    作者声明

     

    作者: feenan

    出处: http://www.cnblogs.com/xuwenmin888

    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

     

     

     

     

     

    分类: Javascript

     
     
  • 相关阅读:
    寒假学习进度8
    寒假学习进度7
    寒假学习进度6
    寒假学习进度5
    寒假学习进度4
    寒假学习进度3
    寒假自学进度13
    Python引用某一文件的方法出现红色波浪线
    寒假自学进度11
    正则表达式(学习)
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3757419.html
Copyright © 2020-2023  润新知