本篇文章为大家展示了angular脏检查原理及伪代码实现,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。
10年积累的成都网站设计、网站制作经验,可以快速应对客户对网站的新想法和需求。提供各种问题对应的解决方案。让选择我们的客户得到更好、更有力的网络服务。我虽然不认识你,你也不认识我。但先建设网站后付款的网站建设流程,更有吉隆免费网站建设让你可以放心的选择与我们合作。
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时,如下:
change
$scope.update = function() { $scope.name = 'tom' }
实际上,当用户点击之后,angular内部还会执行$scope.$apply(),从而触发$digest遍历递归,最终触发界面重绘。
手动调用$apply
但是有些情况下,我们不可能直接使用angular内部指令,有两种情况我们需要手动调用$apply,一种是调用angular内置的语法糖,比如$http, $timeout,另一种是我们没有使用angular内部机制去更新了$scope,比如我们用$element.on('click', () => $scope.name = 'lucy')。也就是说“异步”和“机制外”修改$scope属性值之后,我们都要手动调用$apply,虽然我们在调用$timeout的时候,没有手写$apply,但实际上它内部确实调用了$apply:
function($timeout) { // 当我们通过on('click')的方式触发某些更新的时候,可以这样做 $timeout(() => { $scope.name = 'lily' }) // 也可以这样做 $element.on('click', () => { $scope.name = 'david' $scope.$apply() }) }
但是,一定要注意,在递归过程中,绝对不能手动调用$apply,比如在ng-click的函数中,比如在$watch的回调函数中。
伪代码实现
通过上面的讲解,你可能已经对angular里面的脏检查已经了解了,但是我们还是希望更深入,用代码来把事情说清楚。我这里不去抄写angular的源码,而是自己写一段伪代码,这样更有助于理解整个机制。
import { isEqual } from 'lodash' class Scope { constructor() { this.$$dirty = true this.$$count = 0 this.$$watchers = [] } $watch(property, listener, deepEqual) { let watcher = { property, listener, deepEqual, } this.$$watchers.push(watcher) } $digest() { if (this.$$count >= 10) { throw new Error('$digest超过10次') } this.$$watchers.forEach(watcher => { let newValue = eval('return this.' + watcher.property) let oldValue = watcher.oldValue if (watcher.deepEqual && isEqual(newValue, oldValue)) { watcher.dirty = false } else if (newValue === oldValue) { watcher.dirty = false } else { watcher.dirty = true eval('this.' + watcher.property + ' = ' newValue) watcher.listener(newValue, oldValue) // 注意,listener是在newValue赋值给$scope之后执行的 watcher.oldValue = newValue } // 这里的实现和angular逻辑里面有一点不同,angular里面,当newValue和oldValue都为undefined时,listener会被调用,可能是angular里面在$watch的时候,会自动给$scope加上原本没有的属性,因此认为是一次变动 }) this.$$count ++ this.$$dirty = false for (let watcher of this.$$watchers) { if (watcher.dirty) { this.$$dirty = true break } } if (this.$$dirty) { this.$digest() } else { this.$patch() this.$$dirty = true this.$$count = 0 } } $apply() { if (this.$$count) { return // 当$digest执行的过程中,不能触发$apply } this.$$dirty = true this.$$count = 0 this.$digest() } $patch() { // 重绘界面 } }
function ControllerRegister(controllerTemplate, controllerFunction) { let $scope = new Scope() $paser(controllerTemplate, $scope) // 解析controller的模板,把模板中的属性全部都解析出来,并且把这些属性赋值给$scope controllerFunction($scope) // 在controllerFunction内部可能又给$scope添加了一些属性,注意,不能在运行controllerFunction的时候调用$scope.$apply() let properties = Object.keys($scope) // 找出$scope上的所有属性 // 要把$scope上的一些内置属性排除掉 properties = properties.filter(item => item.indexOf('$') !== 0) // 当然,这种排除方法只能保证在用户不使用$作为属性开头的时候有用 properties.forEach(property => { $scope.$watch(property, () => {}, true) }) $scope.$digest() }
上述内容就是angular脏检查原理及伪代码实现,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注创新互联行业资讯频道。
本文题目:angular脏检查原理及伪代码实现
URL标题:http://lswzjz.com/article/gjojed.html