详解angular脏检查原理及伪代码实现 我们经常听到angular的脏检查机制和数据的双向绑定,这两个词似乎已经是它的代名词了。那么从编程层面,这到底是什么鬼? 当$scope的一个属性被改变时,界面可能会更新。那么为什么angular里面,修改$scope上的一个属性,可以引起界面的变化呢?这是angular的数据响应机制决定的。在angular里面就是脏检查机制。而脏检查,和双向绑定离不开。 这里插句题外话,JavaScript里面非常有意思的一种接口,当你修改(或新增)一个对象的某个属性时,会触发该对象里面的setter。如果你对这块不是很了解,可以先学一下 Object.defineProperty ,包括这两年超级火的vuejs也是通过这个接口实现的。它是一个ES5的标准接口。 我们可以设计一种实现,当你修改或赋值$scope的某个属性时,就触发了$scope这个js对象的setter,我们可以自定义这个setter,在setter函数内部,调用某些逻辑去更新界面。同时,为了确保新塞进来的对象也可以被监听到变化,在你赋值时,还要把赋值进来的对象也进行改造,改造为可以被监听的对象。 双向绑定顾名思义是两个过程,一个是将$scope属性值绑定到HTML结构中,当$scope属性值发生变化的时候界面也发生变化;另一个是,当用户在界面上进行操作,例如点击、输入、选择时,自动触发$scope属性的变化(界面也可能跟着变)。而脏检查的作用是“在当$scope属性值发生变化的时候促使界面发生变化”。 angular的数据响应机制 那么,在代码层面,angular是怎么做到监听数据变动然后更新界面的呢?答案是,angular根本不监听数据的变动,而是在恰当的时机从$rootScope开始遍历所有$scope,检查它们上面的属性值是否有变化,如果有变化,就用一个变量dirty记录为true,再次进行遍历,如此往复,直到某一个遍历完成时,这些$scope的属性值都没有变化时,结束遍历。由于使用了一个dirty变量作为记录,因此被称为脏检查机制。 这里面有三个问题: “恰当的时机”是什么时候? 如何做到知道属性值是否有变化? 这个遍历循环是怎么实现的? 要解决这三个问题,我们需要深入了解angular的$watch, $apply, $digest。 $watch绑定要检查的值 简单的说,当一个作用域创建的时候,angular会去解析模板中当前作用域下的模板结构,并且自动将那些插值(如{{text}})或调用(如ng-click="update")找出来,并利用$watch建立绑定,它的回调函数用于决定如果新值和旧值不同时(或相同时)要干什么事。当然,你也可以手动在脚本里面使用$scope.$watch对某个属性进行绑定。它的使用方法如下: $scope.$watch(string|function, listener, objectEquality, prettyPrintExpression) 第一个参数是一个字符串或函数,如果是函数,需要运行后得到一个字符串,这个字符串用于确定将绑定$scope上的哪个属性。listener则是回调函数,表示当这个属性的值发生变化时,执行该函数。objectEquality是一个boolean,为true的时候,会对object进行深检查(懂什么叫深拷贝的话就懂深检查)。第四个参数是如何解析第一个参数的表达式,使用比较复杂,一般不传。 $digest遍历递归 当使用$watch绑定了要检查的属性之后,当这个属性发生变化,就会执行回调函数。但是前面已经说过了,angular里面没有监听这么一说,那么它怎么会被回调呢?它没有用object的setter机制,而是脏检查机制。脏检查的核心,就是$digest循环。当用户执行了某些操作之后,angular内部会调用$digest(),最终导致界面重新渲染。那么它究竟是怎么一回事呢? 调用$watch之后,对应的信息被绑定到angular内部的一个$$watchers中,它是一个队列(数组),而当$digest被触发时,angular就会去遍历这个数组,并且用一个dirty变量记录$$watchers里面记录的那些$scope属性是否有变化,当有变化的时候,dirty被设置为true,在$digest执行结束的时候,它会再检查dirty,如果dirty为true,它会再调用自己,直到dirty为true。但是为了防止死循环,angular规定,当递归发生了10次或以上时,直接抛出一个错误,并跳出循环。 递归流程如下: 判断dirty是否为true,如果为false,则不进行$digest递归。(dirty默认为true) 遍历$$watchers,取出对应的属性值的老值和新值 根据objectEquality进行新老值的对比。 如果两个值不同,则继续往下执行。如果两个值相同,则设置dirty为false。 检查完所有的watcher之后,如果dirty还为true(这一点需要阅读我下面的伪代码) 设置dirty为true 用新值代替老值,这样,在下一轮递归的时候,老值就是这一轮的新值 再次调用$digest 当递归流程结束之后,$digest还要执行: 将变化后的$scope重新渲染到界面 当一个作用域创建完之后,$scope.$digest会被运行一次。dirty的默认值被设定为true,因此,如果你在controller里面使用了$watch,并且进行了属性赋值,往往刷新页面就可以看到$watch的回调函数被执行了。但是,现在问题来了,上面说的“angular内部会调用$digest()”,这个内部是怎么实现的? $apply触发$digest 在我们自己编程时,并不直接使用$digest,而是调用$scope.$apply(),$apply内部会触发$digest递归遍历。同时,你可以给$apply传一个参数,是个函数,这个函数会在$digest开始之前执行。现在回到上面的问题,angular内部怎么触发$digest?实际上,angular里面要求你通过ng-click, ng-modal, ng-keyup等来进行数据的双向绑定,为什么,因为这些angular的内部指令封装了$apply,比如ng-click,它其实包含了document.addEventListener('click')和$scope.$apply()。 当用户在模板里面使用ng-click时,如下: