IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    Morph_Zhou发表于 2016-05-13 19:09:52
    love 0

    文章参考

    • Make Your Own AngularJS, Part 1: Scopes And Digest

    • How AngularJS implements dirty checking and how to replicate it ourselves

    个人博客: www.morphzhou.cn

    0x00 简单啰嗦点

    所谓双向数据绑定,概念上为数据模型到视图的绑定,以及视图到数据模型的绑定。容易理解的说法就是从界面的操作能实时反映到数据,数据的变更能实时展现到界面。比如Angular中的一个双向数据绑定的示例:

    VU5EACPPPQ3DWVDM.png

    {{yourname}}通过ng-model与input的value绑定,当input的value改变的时候<h1>内的值就会相应改变

    6WNKJZC48LUKFBYIH.png

    双向数据绑定的优点是无需进行和单向数据绑定的那些CRUD(Create,Retrieve,Update,Delete)操作

    0x01 双向数据绑定实现机制

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

    • 脏值检测,例如AngularJS

    • Getter/Setter,例如Vue.js

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

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

    0x02 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!');
    } );

    在Scope中有一个对象数组$$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的值和最新的值不相同,那么这次循环就会被认为发现了“脏值”。

    0x03 实现自己的双向数据绑定

    实际上双向数据绑定的功能远远不止这么一些,这里仅仅是极尽简化的版本。如果想实现一个功能较为齐全的,可以参考慕课网上大漠穷秋的一节课程当中的要求。

    A@B2W7X3PAWBAPPRXQQ2.png

    首先我们先要模仿Angular设置自己的scope,我们只需要简单的实现一下$watch,以及$digest方法。$watch函数需要接受两个参数,watchExp和listener。当$watch被调用时,我们需要将它们push进入到Scope的$$watcher数组中。如果没有提供listener,我们会将listener设置为一个空函数,这样一来我们可以$watch所有的变量。接下来我们将会创建$digest。我们需要来检查旧值是否等于新的值,如果二者不相等,监听器就会被触发。我们会一直循环这个过程,直到二者相等。

    var Scope = function() {
        this.$$watchers = [];
    };
     
    Scope.prototype.$watch = function(watchExp, listener) {
        this.$$watchers.push({
            watchExp: watchExp,
            listener: listener || function() {}
        });
    };
     
    Scope.prototype.$digest = function() {
        var dirty;
     
        do {
            dirty = false;
     
            for (var i = 0; i &lt; this.$$watchers.length; i++) {
                var newValue = this.$$watchers[i].watchExp(),
                    oldValue = this.$$watchers[i].last;
     
                if (oldValue !== newValue) {
                    this.$$watchers[i].listener(newValue, oldValue);
     
                    dirty = true;
     
                    this.$$watchers[i].last = newValue;
                }
            }
        } while (dirty);
    };

    如果我们把$digest函数绑定到一个input元素的keyup事件上。

    var $scope = new Scope();
     
    $scope.name = 'Morph_Zhou';
     
    var element = document.querySelectorAll("input");
     
    element[0].onkeyup = function() {
        $scope.name = element[0].value;
     
        $scope.$digest();
    };
     
    $scope.$watch(function() {
        return $scope.name;
    }, function(newValue, oldValue) {
        console.log('Input value updated - it is now ' + newValue);
     
        element[0].value = $scope.name;
    });
     
    var updateScopeValue = function updateScopeValue() {
        $scope.name = 'Morph_Gaming';
        $scope.$digest();
    };

    使用上面的代码,无论何时我们改变了input的值,$scope中的name属性都会相应的发生变化。

    @6YL6N8KK4PW28@HAP12.png



沪ICP备19023445号-2号
友情链接