手机版

深入分析AngularJS的脏检查

时间:2021-09-05 来源:互联网 编辑:宝哥软件园 浏览:

写在开头

至于Angular脏检查,我之前没有仔细研究过,只是听着说Angular会定期检查周期数据,对比前台数据和后台数据,所以性能损失很大。

这是一个大错误。我甚至在新浪前端采访的时候胡说八道,现在我觉得真的很丢人!没有深刻的理解就胡说八道是很尴尬的。

被拒绝是很自然的。

误解纠正

首先,纠正误解。角度不定期触发藏式检查。

仅当UI事件、ajax请求或超时延迟事件触发脏检查时。

为什么叫脏支票?检查脏数据就是脏检查,对比UI和后台数据是否一致!

解释如下:

$watch对象。

Angular每个绑定到UI的数据都会有一个$watch对象。

该对象包含三个参数

Watch={name: ' ',//当前Watch对象观察到的数据名称getnew value : function($ scope){//获取新值.返回新值;},监听器:函数(新值,旧值){//数据更改时要执行的操作.}}getNewValue()获取当前$作用域上的最新值,listener函数获取新值和旧值并执行一些操作。

通常,当我们使用Angular时,侦听器通常是空的,只有当我们需要监视变更事件时,我们才会显式添加侦听器。

每当我们将数据绑定到UI时,angular都会在您的watchList中插入一块$watch。

例如:

span { { user } }/span span { { password } }/span这将插入两个$watch对象。

之后,开始脏检查。

好吧,让我们把脏支票放在一边,看看它之前的东西

双向数据绑定!只有理解Angular的双向数据绑定,才能彻底理解脏检查。

双向数据绑定

Angular实现双向数据绑定。无非就是界面的操作可以真实的反映数据,数据的变化也可以在界面中呈现出来。

从接口到数据的改变是通过回调操作完成的,例如UI事件、ajax请求或超时,而向接口呈现数据是通过脏检查完成的。

这也是我开始改正的误区

只有在触发UI事件、ajax请求或超时延迟时,才会触发脏检查。

请看下面的例子

div ng-controller=' countertrl ' span ng-bind=' counter '/span button ng-click=' counter=counter 1 '递增/button/div function countertrl($ scope){ $ scope . counter=1;}毫无疑问,每次点击按钮,计数器都会为1,因为点击事件会调用couter 1,然后触发脏检查,并向界面返回新值2。

这是一个简单的双向数据绑定过程。

但就这么简单吗?

请看下面的代码

使用“严格”;var app=angular.module('app ',[]);app.directive('myclick ',function(){ return function(scope,element,attr) { element.on('click ',function(){ scope . data;console . log(scope . data)})})app . controller(' appController ',function($ scope){ $ scope . data=0;});div ng-app=' app ' div ng-controller=' app controller ' span { { data } }/span按钮myclickclick/button/div/div在单击后没有响应。

尝试添加范围。console.log(scope.data)后的$ digest();试试看。

显然,数据有所增加。如果使用$apply()?当然可以(稍后将接受$apply和$digest之间的差异)

为什么会这样?

假设没有AngularJS,我们应该怎么做才能实现这个类似的功能?

body button ng-click='增加'增加/button button ng-click='减少'减少/button span ng-bind=' data '/span script src=' http : app . js '/script/body window . onload=function(){ ' use strict ';var scope={ incremente : function(){ this . data;},递减:函数递减(){ this . data-;},data: 0 }函数bind(){ var list=document . queryselectorall('[ng-click]');for (var i=0,l=list.lengthI l;i ) { list[i]。onclick=(function(index){ return func(){ var func=this . GetAttribute(' ng-click ');作用域[func](作用域);apply();} })(I);} } //apply函数apply(){ var list=document . queryselectorall('[ng-bind]');for (var i=0,l=list.lengthI l;i ) { var bindData=list[i]。getAttribute(' ng-bind ');清单[i]。innerHTML=作用域[BiNDDATa];} } bind();apply();}测试一下:

可以看到,我们没有直接使用DOM的onclick方法,而是创建了一个ng-click,然后在bind中取出与ng-click对应的函数,绑定到onclick的事件处理程序上。你为什么要这么做?虽然数据发生了变化,但是还没有填入界面,所以我们需要在这里做一些额外的操作。

此外,由于双向绑定机制,虽然在DOM操作中更新了数据值,但并没有立即反映在接口上,而是通过apply()反映在接口上,从而完成了职责分离,可以认为是单一的职责模式。

在真实的Angular中,ng-click封装了click,然后调用一次apply函数将数据呈现给界面

在Angular的应用功能中,这里首先进行脏检测,看看oldValue和newVlue是否相等。如果它们不相等,那么newValue被反馈到接口,如果侦听器事件通过$watch注册,它将被调用。

脏支票的优缺点

经过我们以上的分析,我们可以总结出:

简单理解,脏检查就是调用$apply()或$digest()一次,在接口上呈现数据的最新值。每次UI事件发生变化,ajax和超时都会触发$apply()。但是,有以下讨论吗?

不断触发脏支票是个好办法吗?很多人认为这是性能的极大损失,不如setter和getter的观察者模式。但是让我们看看下面的例子

span { { CheckEditemsnumber } }/span function Ctrl($ scope){ var list=[];$ scope . checkeditemsnumber=0;for(var I=0;i1000I){ list . push(false);} $ scope . togglechecked=function(flag){ for(var I=0,l=list.lengthI){ list[I]=flag;$ scope.checkedItemsNumber}}}在脏检测的机制下,这个过程没有压力,会等到循环结束,然后更新checkedItemsNumber一次,应用到接口上。但是基于setter的机制就惨了,checkedItemsNumber每次变化都需要更新,所以性能会极低。因此,两种不同的监测方法各有利弊。最好的方法是了解它们各自使用方法的差异,考虑它们的性能差异,避免在不同的业务场景中最有可能造成性能瓶颈的使用。

好了,既然知道了双向数据绑定中脏检查的触发机制,那么脏检查是如何在内部实现的呢?

脏检查的内部实施

首先,构造$scope对象。

Function $scope=function(){}现在,让我们回到$watch的开头。

我们说每个绑定到UI的数据都有一个对应的$watch对象,这个对象会被推入watchList。

它有两个属性功能

GetNewValue(),也称为monitor函数,在值发生变化时勇敢提示并返回新值。listener () listener函数用于在数据更改时响应行为。还有一个字符串属性

名称:watch当前使用的变量名

function $scope(){ this。$ $ WatchList=[];}在Angular框架中,双美元符号前缀$ $表示该变量被认为是私有的,不应在外部代码中调用。

现在我们可以定义$watch方法了。它采用两个函数作为参数,并将它们存储在$$watchers数组中。我们需要在Scope的每个实例上存储这些函数,所以我们需要把它放在Scope的原型上:

$ scope . prototype . $ watch=function(name,getNewValue,listener){ var watch={ name : name,getNewValue : getNewValue,listener : listener };这个。$ $ watch list . push(watch);}另一面是$digest函数。它执行在作用域上注册的所有侦听器。让我们实现它的一个简化版本,遍历所有监听器并调用它们的监听函数:

$ scope . prototype . $ digest=function(){ var list=this。$ $ watchListfor(var i=0,l=list.lengthil;i ){ list[i]。侦听器();}}现在,我们可以添加监听器并运行脏检查。

var Scope=new Scope();范围。$watch(function() { console.log('嘿,我有新值')},function() { console.log('我是侦听器');})范围。$watch(function() { console.log('嘿,我有newValue 2')},function() { console.log('我是listener 2 ');})范围。$ diset();

代码将托管到github,测试文件路径与命令中的路径一致

好的,两个监听都被触发了。

这些本身用处不大。我们想要的是能够检测getNewValue返回的指定值是否真的发生了变化,然后调用监听函数。

然后,我们需要每次在getNewValue()上获取数据的最新值,因此我们需要获取当前的作用域对象

getNewValue=function(作用域){ return scope[this . name];}是监视函数的一般形式:从范围中获取一些值并返回。

$digest的功能是调用这个监控函数,并比较它返回的值和最后一个返回值之间的差异。如果它们不同,那么侦听器是脏的,应该调用它的侦听器函数。

为此,$digest需要记住每个监控函数返回的最后一个值。现在我们已经为每个侦听器创建了一个对象,只需在其上存储最后一个值。以下是$digest的新实现,用于检测每个监控函数值的变化:

$ scope . prototype . $ digest=function(){ var list=this。$ $ watchListfor(var i=0,l=list.lengthI){ var watch=list[I];var new value=watch . getnew value(this);//在第一个渲染界面,进行数据展示。var oldValue=watch.lastif(newValue!=old value){ watch . listener(new value,old value);} watch.last=newValue}}对于每个watch,我们使用getNewValue()并传入scope实例来获取最新的数据值。然后将其与前一个值进行比较,如果不同,则调用getListener,并传入新值和旧值。最后,我们将最后一个属性设置为新返回值,即最新值。

这个$digest被再次调用,最后一个是未定义的,所以必须有一个数据表示。

好,让我们看看这个监控功能是如何工作的

var scope=new $ scope();scope.hello=10范围。$ watch ('hello ',function(scope){//注意,这里要理解的是,这个函数实际上是var new value=watch。获得新的价值(这个);这样,这指的是当前的监听器观察,所以可以得到名称返回范围[this。name]},函数(新值,旧值){console.log('新值: '新值' ~ ~ ~ '旧值: '旧值);})范围。$ digest();scope.hello=10范围。$ digest();scope.hello=20范围。$ digest();运行结果

我们已经意识到Angular作用域的本质:添加侦听器并在摘要中运行它们。

您还可以看到角度范围的几个重要性能特征:

将数据添加到范围本身不会降低性能。如果没有监听程序监视属性,那么它是否在范围内并不重要。Angular不遍历范围的属性,它遍历侦听器。一旦数据绑定到用户界面,就会添加一个监听器。每个getNewValue()都会在$digest中被调用,所以最好关注侦听器的数量以及每个独立的监控函数或表达式的性能。有时候你不需要注册这么多监听器

看我们上面的程序:

$ scope。原型。$ digest=function(){ var list=this .$ $ watchListfor(var i=0,l=list . LengTii){ var watch=list[I];var新值=watch。获取新值(this);//在第一次渲染界面,进行一个数据呈现var old value=watch . last if(new value!=旧值){手表。监听器(新值、旧值);} watch.last=newValue}}我们这样做,就要求每个监听器看都必须注册听众,然而事实是:在有角的应用中,只有少数的监听器需要注册听众。

更改$scope.prototype.$wacth,在这里放置一个空的函数。

$ scope。原型。$ watch=function(name,getNewValue,listener){ var watch={ name : name,getNewValue : getNewValue,listener : listener | | function(){ };这个$观察名单。推(看);}貌似这样已经初步理解了脏检查原理,但是一个重要的问题我们忽视了。

先后注册了两个监听器,第二个监听器的听众改变了第一个监听器对应数据的值,那么这么做会检测的到吗?

看下面的例子

var scope=new $ scope();scope.first=10scope。秒=1;范围$watch('first ',function(scope){ return scope[this。name]},function(newValue,old value){ console。log(' first : new value : ' new value ' ~ ~ ~ ' old value : ' old value);})范围$watch('second ',function(scope){ return scope[this。name]},function(newValue,old value){ scope。first=8;控制台。log('秒:新值: '新值' ~ ~ ~ '旧值: '旧值);})范围$ digest();控制台。日志(范围。第一);控制台。日志(范围。第二);

可以看到,值为8,1,已经发生改变,但是界面上的值却没有改变。

现在来修复这个问题。

当数据脏的时候持续摘要

我们需要改变一下消化,让它持续遍历所有监听器,直到监控的值停止变更。

首先,我们把现在的$摘要函数改名为$$digestOnce,它把所有的监听器运行一次,返回一个布尔值,表示是否还有变更了。

$ scope。原型。$ $ digest once=function(){ var dirty;定义变量列表=这个$ $ watchListfor(var i=0,l=list.lengthilI){ var watch=list[I];var新值=watch。获取新值(这。姓名);var old value=watch . last if(new value!==旧值){手表。监听器(新值、旧值);//因为听众操作,已经检查过的数据可能变脏dirty=true} watch.last=newValue返脏;}};然后,我们重新定义$digest,它作为一个"外层循环"来运行,当有变更发生的时候,调用$$digestOnce:

$ scope。原型。$ digest=function(){ var dirty=true;而(脏){脏=这个$ $ digestOnce();} };$摘要现在至少运行每个监听器一次了。如果第一次运行完,有监控值发生变更了,标记为肮脏的,所有监听器再运行第二次。这会一直运行,直到所有监控的值都不再变化,整个局面稳定下来了。

在有角的作用域里并不是真的有个函数叫做$$digestOnce,相反,文摘循环都是包含在$摘要里的。我们的目标更多是清晰度而不是性能,所以把内层循环封装成了一个函数。

测试一下

var scope=new $ scope();scope.first=10scope。秒=1;范围$watch('first ',function(scope){ return scope[this。name]},function(newValue,old value){ console。log(' first : new value : ' new value ' ~ ~ ~ ' old value : ' old value);})范围$watch('second ',function(scope){ return scope[this。name]},function(newValue,old value){ scope。first=8;控制台。log('秒:新值: '新值' ~ ~ ~ '旧值: '旧值);})范围$ digest();控制台。日志(范围。第一);控制台。日志(范围。第二);

可以看到,现在界面上的数据已经全部为最新

现在,我们可以对Angular侦听器有另一个重要的理解:它们可能在一个摘要中执行多次。这就是为什么人们常说听者应该是幂等的:听者应该没有边界效应,或者边界效应应该只出现有限的次数。例如,假设一个监视器函数触发了一个Ajax请求,并且不可能确定您的应用程序已经发出了多少请求。

如果两个听众循环变化呢?现在的情况是:

var scope=new $ scope();scope.first=10scope . second=1;范围。$watch('first ',function(scope){ return scope[this . name]},function(newValue,old value){ scope . second;})范围。$watch('second ',function(scope){ return scope[this . name]},function(newValue,old value){ scope . first;})那么,脏检就不会停止,还会继续循环。怎么解决?

更稳定的$digest

我们需要做的是在可接受的迭代次数内控制摘要的操作。如果这么多次后范围还在变化,就勇敢放手,声明永远不稳定。此时,我们将抛出一个异常,因为无论范围的状态变成什么,都不可能是用户想要的结果。

迭代的最大值称为生存时间。默认值是10,可能有点小(我们刚刚运行了100,000次这个摘要!),但是请记住,这是一个性能敏感的地方,因为摘要经常被执行,并且每个摘要运行所有的侦听器。用户也不太可能创建超过10个链监听器。

让我们继续向外部摘要循环添加一个循环计数器。如果达到TTL,将引发异常:

$ scope . prototype . $ digest=function(){ var dirty=true;var CheckTimes=0;而(脏){脏=这个。$ $ digestOnce();检查时间;If(检查时间10脏){抛出新错误('检查10次以上');console . log(' 123 ');} };};测试一下

var scope=new $ scope();scope . first=1;scope.second=10范围。$watch('first ',function(scope){ return scope[this . name]},function(newValue,old value){ scope . second;console . log(' first : new value : ' new value ' ~ ~ ~ ' ' old value : ' old value);})范围。$watch('second ',function(scope){ return scope[this . name]},function(newValue,old value){ scope . first;console . log(' second : new value : ' new value ' ~ ~ ~ ~ ' old value : ' old value);})范围。$ digest();

好了,这里介绍一下角度脏检查和双向数据绑定的原理。虽然与真实的Angular相差甚远,但基本可以说明原理。希望对大家的学习有帮助,希望大家多多支持我们。

版权声明:深入分析AngularJS的脏检查是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。