详解Angular系列之变化检测(Change Detection)
概述
简单来说变化检测就是angular用来检测视图与模型之间绑定的值是否发生了改变,当检测到模型中绑定的值发生改变时,则同步到视图上,反之,当检测到视图上绑定的值发生改变时,则回调对应的绑定函数。
什么情况下会引起变化检测?
总结起来, 主要有如下几种情况可能也改变数据:
- 用户输入操作,比如点击,提交等
- 请求服务端数据(xhr)
- 定时事件,比如settimeout,setinterval
上述三种情况都有一个共同点,即这些导致绑定值发生改变的事件都是异步发生的。如果这些异步的事件在发生时能够通知到angular框架,那么angular框架就能及时的检测到变化。
左边表示将要运行的代码,这里的stack表示javascript的运行栈,而webapi则是浏览器中提供的一些javascript的api,taskqueue表示javascript中任务队列,因为javascript是单线程的,异步任务在任务队列中执行。
具体来说,异步执行的运行机制如下:
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之 中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
当上述代码在javascript中执行时,首先func1 进入运行栈,func1执行完毕后,settimeout进入运行栈,执行settimeout过程中将回调函数cb 加入到任务队列,然后settimeout出栈,接着执行func2函数,func2函数执行完毕时,运行栈为空,接着任务队列中cb 进入运行栈得到执行。可以看出异步任务首先会进入任务队列,当运行栈中的同步任务都执行完毕时,异步任务进入运行栈得到执行。如果这些异步的任务执行前与执行后能提供一些钩子函数,通过这些钩子函数,angular便能获知异步任务的执行。
angular2 获取变化通知
那么问题来了,angular2是如何知道数据发生了改变?又是如何知道需要修改dom的位置,准确的最小范围的修改dom呢?没错,尽可能小的范围修改dom,因为操作dom对于性能来说可是一件奢侈品。
在angularjs中是由代码$scope.$apply()或者$scope.$digest触发,而angular接入了zonejs,由它监听了angular所有的异步事件。
zonejs是怎么做到的呢?
实际上zone有一个叫猴子补丁的东西。在zone.js运行时,就会为这些异步事件做一层代理包裹,也就是说zone.js运行后,调用settimeout、addeventlistener等浏览器异步事件时,不再是调用原生的方法,而是被猴子补丁包装过后的代理方法。代理里setup了钩子函数, 通过这些钩子函数, 可以方便的进入异步任务执行的上下文.
//以下是zone.js启动时执行逻辑的抽象代码片段 function zoneawareaddeventlistener() {...} function zoneawareremoveeventlistener() {...} function zoneawarepromise() {...} function patchtimeout() {...} window.prototype.addeventlistener=zoneawareaddeventlistener; window.prototype.removeeventlistener=zoneawareremoveeventlistener; window.prototype.promise = zoneawarepromise; window.prototype.settimeout = patchtimeout;
变化检测的过程
angular的核心是组件化,组件的嵌套会使得最终形成一棵组件树。angular的变化检测可以分组件进行,每一个component都对应有一个changedetector,我们可以在component中通过依赖注入来获取到changedetector。而我们的多个component是一个树状结构的组织,由于一个component对应一个changedetector,那么changedetector之间同样是一个树状结构的组织.
另外,angular的数据流是自顶而下,从父组件到子组件单向流动。单向数据流向保证了高效、可预测的变化检测。尽管检查了父组件之后,子组件可能会改变父组件的数据使得父组件需要再次被检查,这是不被推荐的数据处理方式。在开发模式下,angular会进行二次检查,如果出现上述情况,二次检查就会报错:expression changed after it has been checked error。而在生产环境中,脏检查只会执行一次。
相比之下,angularjs采用的是双向数据流,错综复杂的数据流使得它不得不多次检查,使得数据最终趋向稳定。理论上,数据可能永远不稳定。angularjs给出的策略是,脏检查超过10次,就认为程序有问题,不再进行检查。
变化检测策略
angular有两种变化检测策略。default是angular默认的变化检测策略,也就是上述提到的脏检查,只要有值发生变化,就全部从父组件到所有子组件进行检查,。另一种更加高效的变化检测方式:onpush。onpush策略,就是只有当输入数据(即@input)的引用发生变化或者有事件触发时,组件才进行变化检测。
defalut 策略
main.component.ts
@component({ selector: 'app-root', template: ` <h1>变更检测策略</h1> <p>{{ slogan }}</p> <button type="button" (click)="changestar()"> 改变明星属性 </button> <button type="button" (click)="changestarobject()"> 改变明星对象 </button> <movie [title]="title" [star]="star"></movie>`, }) export class appcomponent { slogan: string = 'change detection'; title: string = 'default 策略'; star: star = new star('周', '杰伦'); changestar() { this.star.firstname = '吴'; this.star.lastname = '彦祖'; } changestarobject() { this.star = new star('刘', '德华'); } }
movie.component.ts
@component({ selector: 'movie', styles: ['div {border: 1px solid black}'], template: ` <div> <h3>{{ title }}</h3> <p> <label>star:</label> <span>{{star.firstname}} {{star.lastname}}</span> </p> </div>`, }) export class moviecomponent { @input() title: string; @input() star; }
上面代码中, 当点击第一个按钮改变明星属性时,依次对slogan, title, star三个属性进行检测, 此时三个属性都没有变化, star没有发生变化,是因为实质上在对star检测时只检测star本身的引用值是否发生了改变,改变star的属性值并未改变star本身的引用,因此是没有发生变化。
而当我们点击第二个按钮改变明星对象时 ,重新new了一个 star ,这时变化检测才会检测到 star发生了改变。
然后变化检测进入到子组件中,检测到star.firstname和star.lastname发生了变化, 然后更新视图.
onpush策略
与上面代码相比, 只在movie.component.ts中的@component中增加了一行代码:
changedetection:changedetectionstrategy.onpush
此时, 当点击第一个按钮时, 检测到star没有发生变化, ok,变化检测到此结束, 不会进入到子组件中, 视图不会发生变化.
当点击第二个按钮时,检测到star发生了变化, 然后变化检测进入到子组件中,检测到star.firstname和star.lastname发生了变化, 然后更新视图.
所以,当你使用了onpush检测机制时,在修改一个绑定值的属性时,要确保同时修改到了绑定值本身的引用。但是每次需要改变属性值的时候去new一个新的对象会很麻烦,immutable.js 你值得拥有!
变化检测对象引用
通过引用变化检测对象changedetectorref,可以手动去操作变化检测。我们可以在组件中的通过依赖注入的方式来获取该对象:
constructor( private changeref:changedetectorref ){}
变化检测对象提供的方法有以下几种:
- markforcheck() - 在组件的 metadata 中如果设置了 changedetection:changedetectionstrategy.onpush 条件,那么变化检测不会再次执行,除非手动调用该方法, 该方法的意思是在变化监测时必须检测该组件。
- detach() - 从变化检测树中分离变化检测器,该组件的变化检测器将不再执行变化检测,除非手动调用 reattach() 方法。
- reattach() - 重新添加已分离的变化检测器,使得该组件及其子组件都能执行变化检测
- detectchanges() - 从该组件到各个子组件执行一次变化检测
onpush策略下手动发起变化检测
组件中添加事件改变输入属性
在上面代码movie.component.ts中修改如下
@component({ selector: 'movie', styles: ['div {border: 1px solid black}'], template: ` <div> <h3>{{ title }}</h3> <p> <button (click)="changestar()">点击切换名字</button> <label>star:</label> <span>{{star.firstname}} {{star.lastname}}</span> </p> </div>`, changedetection:changedetectionstrategy.onpush }) export class moviecomponent { constructor( private changeref:changedetectorref ){} @input() title: string; @input() star; changestar(){ this.star.lastname = 'xjl'; } }
此时点击按钮切换名字时,star更改如下
![图片描述][3]
第二种就是上面讲到的使用变化检测对象中的 markforcheck()方法.
ngoninit() { setinterval(() => { this.star.lastname = 'xjl'; this.changeref.markforcheck(); }, 1000); }
输入属性为observable
修改app.component.ts
@component({ selector: 'app-root', template: ` <h1>变更检测策略</h1> <p>{{ slogan }}</p> <button type="button" (click)="changestar()"> 改变明星属性 </button> <button type="button" (click)="changestarobject()"> 改变明星对象 </button> <movie [title]="title" [star]="star" [addcount]="count"></movie>`, }) export class appcomponent implements oninit{ slogan: string = 'change detection'; title: string = 'onpush 策略'; star: star = new star('周', '杰伦'); count:observable<any>; ngoninit(){ this.count = observable.timer(0, 1000) } changestar() { this.star.firstname = '吴'; this.star.lastname = '彦祖'; } changestarobject() { this.star = new star('刘', '德华'); } }
此时,有两种方式让moviecomponent进入检测,一种是使用变化检测对象中的 markforcheck()方法.
ngoninit() { this.addcount.subscribe(() => { this.count++; this.changeref.markforcheck(); })
另外一种是使用async pipe 管道
@component({ selector: 'movie', styles: ['div {border: 1px solid black}'], template: ` <div> <h3>{{ title }}</h3> <p> <button (click)="changestar()">点击切换名字</button> <label>star:</label> <span>{{star.firstname}} {{star.lastname}}</span> </p> <p>{{addcount | async}}</p> </div>`, changedetection: changedetectionstrategy.onpush })
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。