AngularJS源码分析之{{双向数据绑定}}

双向数据绑定实现机制

目前对于双向数据绑定的实现有这么几种流派

  • 脏值检测,例如AngularJS

  • Getter/Setter,例如Vue.js

对于Getter/Setter实现的数据双向绑定来说,核心在于重定义model的getter与setter方法,在数据变动的时候重新渲染页面。两种方式各有优劣。

当我们使用Getter/Setter的时候,每次修改数值都会激活刷新模版的方法,而脏值检测则可以在完成所有数值变动后,统一刷新到Dom。但是当监听元素变多的时候,watcher列表会变得很长,查询变动的数据元素将耗费更多的资源。

AngularJS双向数据绑定源码分析

源码版本 Angular-1.5.0 angular.js

在Angular当中,有个贯穿始终的对象$scope。Scope本质为一个构造函数,而$scope就是Scope的实例。源码16028行

function 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的原型(Scope.prototype)中共定义了13个函数。其中有两个函数对双向数据绑定起着至关重要的作用:监视对象属性

  • $watch

  • $digest

$watch$digest是同一个硬币的两面。它们二者同时形成了$digest循环的核心:对数据的变化做出反应。可以使用$watch函数为scope添加一个监视器。当这个scope中有变化发生时,监视器便会提醒你。

$watch 源码16247行

$watch: function(watchExp, listener, objectEquality, prettyPrintExpression) {
        var get = $parse(watchExp);
 
        if (get.$$watchDelegate) {
          return get.$$watchDelegate(this, listener, objectEquality, get, watchExp);
        }
        var scope = this,
            array = scope.$$watchers,
            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() {
          if (arrayRemove(array, watcher) >= 0) {
            incrementWatchersCount(scope, -1);
          }
          lastDirtyWatch = null;
        };
}

为了监视一个变量的变化,可以使用$scope.$watch函数。这个函数的前两个,它指明了要观察什么(watchExp),在变化时要发生什么(listener)。

$scope.name = 'Morph_Zhou';
 
$scope.$watch( function( ) {
    return $scope.name;
}, function( newValue, oldValue ) {
    console.log('$scope.name was updated!');
} );

cope中有一个对象数组$$watchers,里面保存着我们定义的所有的监视器对象watcher$watch函数将会返回一个deregisterWatch函数。这意味着如果我们使用$scope.$watch对一个变量进行监视,我们也可以在以后通过调用某个函数来停止监视。

另外一个是$digest函数。它迭代了所有绑定到scope中的监视器,然后进行监视并运行相应的监听函数。

$digest 源码16607行

$digest: function() {
        var watch, value, last, fn, get,
            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();
 
        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
          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) {
                    get = watch.get;
                    if ((value = 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;
                      fn = watch.fn;
                      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--)) {
            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);
          }
        }
}
$digest 函数将会在 $rootScope 中被 $scope . $apply 所调用。它将会在 $rootScope 中运行 digest 循环,然后向下遍历每一个作用域并在每个作用域上运行循环。在简单的情形中, digest 循环将会触发所有位于 $$watchers 变量中的所有 watchExp 函数,将它们和最新的值进行对比,如果值不相同,就会触发监听器。当 digest 循环运行时,它将会遍历所有的监听器然后再次循环,只要这次循环发现了”脏值”,循环就会继续下去。如果 watchExp 的值和最新的值不相同,那么这次循环就会被认为发现了“脏值”。


转自:https://segmentfault.com/a/1190000005119289

猜你喜欢

转载自blog.csdn.net/qq_35448976/article/details/80299296