AngularJS可以记住value值并且会把它和之前的value值进行比较。这就是基本的脏检查机制。如果某处的value值发生了变化,那么AngularJS就会触发指定事件。
$apply()
这个方法是用来处理AngularJS框架之外的表达式的,与它相辅相成的还有$digest()
方法。一次digest就是一次完全的脏检查,它可以运行在所有的浏览器中。
关于$watch
每一次你在UI中绑定什么东西时你就会往$watch
的队列中插入一条$watch
,想象一下$watch就是在所监测的model中可以侦查数据变化的东西。比如说:
1 2
| User: <input type="text" ng-model="user" /> Password: <input type="password" ng-model="pass" />
|
在这里我们分别给两个input绑定了$scope.user和$scope.pass,就是说我们向$watch队列添加了两个$watch。
每一个绑定到了UI上的数据都会生成一个$watch,我们的模板加载完成时,也就是在linking阶段,Angular解释器会寻找每一个directive并且创造它们所需的$watch。
一个watcher包含了三个东西:
1 2 3 4 5 6 7 8 9
| $$watchers = [ { eq: false, // 表明我们是否需要检查对象级别的相等 fn: function( newValue, oldValue ) {}, // 这是我们提供的监听器函数 last: 'Ryan', // 变量的最新值 exp: function(){}, // 我们提供的watchExp函数 get: function(){} // Angular's编译后的watchExp函数 } ];
|
定义监听器的几种方法:
1.把$watch设置为$scope的一种属性:$scope.$watch('person.username', validateUnique);
2.插入angular表达式:<p>username: </p>
3.使用类似于ng-model的指令来定义监听器:<input ng-model="person.username />
关于$digest和$apply
如果你点击一个按钮,或者在一个input框中输入,事件的回调函数会在javascript中运行,并且你可以做任意的DOM操作,当回调函数结束时,浏览器会相应地在DOM中做出改变。
当一个控制器/指令/等等东西在AngularJS中运行时,AngularJS内部会运行一个叫做$scope.$apply的函数。这个$apply函数会接收一个函数作为参数并运行它,在这之后才会在rootScope上运行$digest函数。
AngularJS的$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可带参数。
$digest函数将会在$rootScope中被$scope.$apply所调用。它将会在$rootScope中运行digest循环,然后向下遍历每一个作用域并在每个作用域上运行循环。在简单的情形中,digest循环将会触发所有位于$$watchers变量中的所有watchExp函数,将它们和最新的值进行对比,如果值不相同,就会触发监听器。$digest函数检查$watch队列中的所有监听器最新的value值,一次$digest循环是被指令触发的。如果表达式新的value值与之前不同,就会调用监听器的函数,这个函数可能是重新编译部分的DOM,重新计算$scope的值,激活一个AJAX请求,或者任何你想做的事。
监听器函数可以修改$scope或是父$scope的其他属性,一旦有出发了一个监听器函数,我们就无法保证其它的$scope也是干净的,所以我们会再次执行整个digest循环。
$apply与$digest作用类似,$apply会使ng进入$digest cycle, 并从$rootScope开始遍历(深度优先)检查数据变更。不同之处在于$apply可以带参数,并且会触发作用域上的所有监控,\$digest仅仅触发当前作用域和子作用域的监控。
build your own dirty-checking
了解以上知识后,我们可以自己写一个具有基本功能的脏检测了。
首先定义Scope,然后扩展这个函数的原型对象来复制\$digest和\$watch
1 2 3 4 5 6 7 8 9 10 11
| var Scope = function( ) { this.$$watchers = []; }; Scope.prototype.$watch = function( ) { }; Scope.prototype.$digest = function( ) { };
|
设置\$watch函数,它接收watchExp和listener这两个参数,被调用时我们会把其push到$$watchers数组中。因此代码扩展为:
1 2 3 4 5 6
| Scope.prototype.$watch = function( watchExp, listener ) { this.$$watchers.push( { watchExp: watchExp, listener: listener || function() {} } ); };
|
如果没有传入listener的话我们会把它设置为空函数。
$digest用来检查新值旧值是否相等,如果不相等则触发监听器,不断循环这个过程,直到新值旧值相等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| Scope.prototype.$digest = function( ) { var dirty; do { dirty = false; for( var i = 0; i < 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); };
|
下一步我们需要创建一个作用域的实例,并把实例赋值给$scope,然后注册监听函数,使得更新$scope之后运行$digest
1 2 3 4 5 6 7 8 9 10 11
| var $scope = new Scope(); $scope.name = 'Ryan'; $scope.$watch(function(){ return $scope.name; }, function( newValue, oldValue ) { console.log(newValue, oldValue); } ); $scope.$digest();
|
我们发现在控制台输出了Ryan undefined
,成功了!
最后我们可以把$digest函数绑定到事件上,比如input元素的keyup事件,即意味着我们可以实现双向数据绑定!
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
| 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 < 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); }; var $scope = new Scope(); $scope.name = 'Ryan'; 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 = 'Bob'; $scope.$digest(); };
|
参考内容