文章转自:http://www.2cto.com/kf/201408/327594.html

AngularJs 的元素与模型双向绑定依赖于循环检测它们之间的值,这种做法叫做脏检测,这几天研究了一下其源码,将 Angular 的实现分享一下。

 
首先看看如何将 Model 的变更更新到 UI
 
Angular 的 Model 是一个 Scope 的类型,每个 Scope 都归属于一个 Directive 对象,比如 $rootScope 就归属于 ng-app。
 
从 ng-app 往下,每个 Directive 创建的 Scope 都会一层一层链接下去,形成一个以 $rootScope 为根的链表,注意 Scope 还有同级的概念,形容更贴切我觉得应该是一棵树。
 
我们大概看一下 Scope 都有哪些成员:
 
  function Scope() {
      this.$id = nextUid();
      // 依次为: 阶段、父 Scope、Watch 函数集、下一个同级 Scope、上一个同级 Scope、首个子级 Scope、最后一个子级 Scope
      this.$$phase = this.$parent = this.$$watchers =
                     this.$$nextSibling = this.$$prevSibling =
                     this.$$childHead = this.$$childTail = null;
          // 重写 this 属性以便支持原型链
      this['this'] = this.$root =  this;
      this.$$destroyed = false;
      // 以当前 Scope 为上下文的异步求值队列,也就是一堆 Angular 表达式
      this.$$asyncQueue = [];
      this.$$postDigestQueue = [];
      this.$$listeners = {};
      this.$$listenerCount = {};
      this.$$isolateBindings = {};
}
Scope.$digest,这是 Angular 提供的从 Model 更新到 UI 的接口,你从哪个 Scope 调用,那它就会从这个 Scope 开始遍历,通知模型更改给各个 watch 函数,
来看看 $digest 的源码:
 
$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');
 
    // 最后一个检测到脏值的 watch 函数
    lastDirtyWatch = null;
 
    // 开始脏检测,只要还有脏值或异步队列不为空就会一直循环
    do {
      dirty = false;
      // 当前遍历到的 Scope
      current = target;
 
      // 处理异步队列中所有任务, 这个队列由 scope.$evalAsync 方法输入
      while(asyncQueue.length) {
        try {
          asyncTask = asyncQueue.shift();
          asyncTask.scope.$eval(asyncTask.expression);
        } catch (e) {
          clearPhase();
          $exceptionHandler(e);
        }
        lastDirtyWatch = null;
      }
 
      traverseScopesLoop:
      do {
        // 取出当前 Scope 的所有 watch 函数
        if ((watchers = current.$$watchers)) {
          length = watchers.length;
          while (length--) {
            try {
              watch = watchers[length];
 
              if (watch) {
                // 1.取 watch 函数的运算新值,直接与 watch 函数最后一次值比较
                // 2.如果比较失败则尝试调用 watch 函数的 equal 函数,如果没有 equal 函数则直接比较新旧值是否都是 number
                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 函数的变更通知函数, 也就是说各个 directive 从这里更新 UI
                  watch.fn(value, ((last === initWatchVal) ? value : last), current);
 
                  // 当 digest 调用次数大于 5 的时候(默认10),记录下来以便开发人员分析。
                  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
 
        // 没有子级 Scope,也没有同级 Scope
        if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) {
          // 又判断一遍不知道为什么,不过这个时候 next === undefined 了,也就退出当前 Scope 的 watch 遍历了
          while(current !== target && !(next = current.$$nextSibling)) {
            current = current.$parent;
          }
        }
      } while ((current = next));
 
 
      // 当 TTL 用完,依旧有未处理的脏值和异步队列则抛出异常
      if((dirty || asyncQueue.length) && !(ttl--)) {
        clearPhase();
        throw $rootScopeMinErr('infdig',
            '{0} $digest() iterations reached. Aborting!\n' +
            'Watchers fired in the last 5 iterations: {1}',
            TTL, toJson(watchLog));
      }
 
    } while (dirty || asyncQueue.length);
 
    // 退出 digest 阶段,允许其他人调用
    clearPhase();
 
    while(postDigestQueue.length) {
      try {
        postDigestQueue.shift()();
      } catch (e) {
        $exceptionHandler(e);
      }
    }
  }
虽然看起来很长,但是很容易理解,默认从 $rootScope 开始遍历,对每个 watch 函数求值比较,出现新值则调用通知函数,由通知函数更新 UI,我们来看看 ng-model 是怎么注册通知函数的:
 
$scope.$watch(function ngModelWatch() {
    var value = ngModelGet($scope);
 
    // 如果 ng-model 当前记录的 modelValue 不等于 Scope 的最新值
    if (ctrl.$modelValue !== value) {
 
      var formatters = ctrl.$formatters,
          idx = formatters.length;
 
      // 使用格式化器格式新值,比如 number,email 之类
      ctrl.$modelValue = value;
      while(idx--) {
        value = formatters[idx](value);
      }
 
      // 将新值更新到 UI
      if (ctrl.$viewValue !== value) {
        ctrl.$viewValue = value;
        ctrl.$render();
      }
    }
 
    return value;
});
那么 UI 更改如何更新到 Model 呢
 
很简单,靠 Directive 编译时绑定的事件,比如 ng-model 绑定到一个输入框的时候事件代码如下:
 
var ngEventDirectives = {};
forEach(
  'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
function(name) {
        var directiveName = directiveNormalize('ng-' + name);
        ngEventDirectives[directiveName] = ['$parse', function($parse) {
     return {
       compile: function($element, attr) {
         var fn = $parse(attr[directiveName]);
         return function(scope, element, attr) {
// 触发以上指定的事件,就将元素的 scope 和 event 对象一起发送给 direcive
           element.on(lowercase(name), function(event) {
             scope.$apply(function() {
               fn(scope, {$event:event});
             });
           });
         };
       }
         };
    }];
  }
);
Directive 接收到输入事件后根据需要再去 Update Model 就好啦。
 
相信经过以上研究应该对 Angular 的绑定机制相当了解了吧,现在可别跟人家说起脏检测就觉得是一个 while(true) 一直在求值效率好低什么的,跟你平时用事件没啥两样,多了几次循环而已。
 
最后注意一点就是平时你通常不需要手动调用 scope.$digest,特别是当你的代码在一个 $digest 中被回调的时候,因为已经进入了 digest 阶段所以你再调用则会抛出异常。
我们只在没有 Scope 上下文的代码里边需要调用 digest,因为此时你对 UI 或 Model 的更改 Angular 并不知情。

Angularjs 双向绑定机制解析的更多相关文章

  1. 深入学习AngularJS中数据的双向绑定机制

    来自:http://www.jb51.net/article/80454.htm Angular JS (Angular.JS) 是一组用来开发Web页面的框架.模板以及数据绑定和丰富UI组件.它支持 ...

  2. angularjs深入理解向指令传递数据,双向绑定机制

    <!DOCTYPE html> <html lang="zh-CN" ng-app="app"> <head> <me ...

  3. AngularJS双向绑定,手动实施观察

    实现这样的一个需求:页面中某个地方显示某个文本框的值经过计算得到的结果,而且是文本框值每次变化显示的计算结果也跟着动态变化. 在controller中可以声明一个对象,它的一个字段用来存储初始值: $ ...

  4. angularJS双向绑定和依赖反转

    一.双向绑定: UI<-->数据 数据->UI (数据改变UI跟着变) UI->数据 (UI改变数据跟着变) 数据改变->UI改变原理: 监听数据是否改变,如果改变更新U ...

  5. AngularJs双向绑定详解

    双向绑定的三个重要方法: $scope.$apply() $scope.$digest() $scope.$watch() 一.$scope.$watch() 我理解的$watch就是将对某个数据的监 ...

  6. vue的双向绑定原理解析(vue项目重构二)

    现在的前端框架 如果没有个数据的双向/单向绑定,都不好意思说是一个新的框架,至于为什么需要这个功能,从jq或者原生js开始做项目的前端工作者,应该是深有体会. 以下也是个人对vue的双向绑定原理的一些 ...

  7. mvvm双向绑定机制的原理和代码实现

    mvvm框架的双向绑定,即当对象改变时,自动改变相关的dom元素的值,反之,当dom元素改变时,能自动更新对象的值,当然dom元素一般是指可输出的input元素. 1. 首先实现单向绑定,在指定对象的 ...

  8. AngularJs双向绑定

    模型数据(Data) 模型是从AngularJS作用域对象的属性引申的.模型中的数据可能是Javascript对象.数组或基本类型,这都不重要,重要的是,他们都属于AngularJS作用域对象. An ...

  9. AngularJS 脏检查机制

    脏检查是AngularJS的核心机制之一,它是实现双向绑定.MVVM模式的重要基础. 一.digest循环 AngularJS将双向绑定转换为一个堆watch表达式,然后递归检查这些watch表达式的 ...

随机推荐

  1. 如何通过ShadowSocket自动更新Chrome

    经常收到Chrome的更新提示,并下载更新程序后,报无法连接网络,然后更新不能了, 经过一段时间的搜索,找到了一条比较好的方法,分享一下: 1. 本机打开ShadowSocket 2. 打开Privo ...

  2. APIPA(Automatic Private IP Addressing,自动专用IP寻址)

    APIPA APIPA(Automatic Private IP Addressing,自动专用IP寻址),是一个DHCP故障转移机制.当DHCP服务器出故障时, APIPA在169.254.0.1到 ...

  3. WCF开发那些需要注意的坑 Z

    执行如下 批处理:"C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bin\svcutil.exe" http://127.0.0.1: ...

  4. [React] 多组件生命周期转换关系

    前段时间一直在基于React做开发,最近得空做一些总结,防止以后踩坑. 言归正传,React生命周期是React组件运行的基础,本文主要是归纳多组件平行.嵌套时,生命周期转换关系. 生命周期 Reac ...

  5. 在Vi里面实现字符串的批量替换

    在Vi里面实现字符串的批量替换. a. 文件内全部替换: %s#abc#def#g(用def替换文件中所有的abc) 例如把一个文本文件里面的"linuxidc.com"全部替换成 ...

  6. yii 核心类classes.php详解(持续更新中...)

    classes.php在yii运行的时候将被自动加载,位于yii2文件夹底下. <?php /** * Yii core class map. * * This file is automati ...

  7. C#中的try catch 和finally

    错误的出现并不总是编写应用程序的人的原因,有时应用程序会因为终端用户的操作而发生错误.无论如何,我们都应预测应用程序和代码中出现的错误. 这三个关键字try是必定要用的,要不然就失去了意义.然后cat ...

  8. Mysql 视图 游标 触发器 存储过程 事务

    Mysql 视图 触发器 存储过程 游标 游标是从数据表中提取出来的数据,以临时表的形式存放在内存中,在游标中有一个数据指针,在初始状态下指向的是首记录,利用fetch语句可以移动该指针,从而对游标中 ...

  9. 刚开始用git遇到的无法提交变更的问题

    原来我在目录里打开命令行,git bash默认执行的目录是c:/users了,错误的使用了git init,把$HOME 路径下的所有文件载入 git 仓库了,删除$HOME 路径下的".g ...

  10. RabbitMQ简介

    AMQP简介 在了解RabbitMQ之前,首先要了解AMQP协议.AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消 ...