详解Angular系列之变化检测(Change Detection)


Posted in Javascript onFebruary 26, 2018

概述

简单来说变化检测就是Angular用来检测视图与模型之间绑定的值是否发生了改变,当检测到模型中绑定的值发生改变时,则同步到视图上,反之,当检测到视图上绑定的值发生改变时,则回调对应的绑定函数。

什么情况下会引起变化检测?

总结起来, 主要有如下几种情况可能也改变数据:

  1. 用户输入操作,比如点击,提交等
  2. 请求服务端数据(XHR)
  3. 定时事件,比如setTimeout,setInterval

上述三种情况都有一个共同点,即这些导致绑定值发生改变的事件都是异步发生的。如果这些异步的事件在发生时能够通知到Angular框架,那么Angular框架就能及时的检测到变化。

详解Angular系列之变化检测(Change Detection)

左边表示将要运行的代码,这里的stack表示Javascript的运行栈,而webApi则是浏览器中提供的一些Javascript的API,TaskQueue表示Javascript中任务队列,因为Javascript是单线程的,异步任务在任务队列中执行。

具体来说,异步执行的运行机制如下:

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之 中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

当上述代码在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系列之变化检测(Change Detection)

变化检测策略

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
 ){}

变化检测对象提供的方法有以下几种:

  1. markForCheck() - 在组件的 metadata 中如果设置了 changeDetection:ChangeDetectionStrategy.OnPush 条件,那么变化检测不会再次执行,除非手动调用该方法, 该方法的意思是在变化监测时必须检测该组件。
  2. detach() - 从变化检测树中分离变化检测器,该组件的变化检测器将不再执行变化检测,除非手动调用 reattach() 方法。
  3. reattach() - 重新添加已分离的变化检测器,使得该组件及其子组件都能执行变化检测
  4. 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
})

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
新页面打开实际尺寸的图片
Aug 25 Javascript
extjs 如何给column 加上提示
Jul 29 Javascript
javascript数组去重方法汇总
Apr 23 Javascript
JS实现的仿淘宝交易倒计时效果
Nov 27 Javascript
jQuery实现页面顶部显示的进度条效果完整实例
Dec 09 Javascript
Node.js实现数据推送
Apr 14 Javascript
JS实现滑动门效果的方法详解
Dec 19 Javascript
JavaScript数据结构学习之数组、栈与队列
May 02 Javascript
对存在JavaScript隐式类型转换的四种情况的总结(必看篇)
Aug 31 Javascript
bootstrap日期插件daterangepicker使用详解
Oct 19 Javascript
angularjs 动态从后台获取下拉框的值方法
Aug 13 Javascript
详解微信小程序网络请求接口封装实例
May 02 Javascript
Bootstrap4如何定制自己的颜色和风格
Feb 26 #Javascript
vue-cli下的vuex的简单Demo图解(实现加1减1操作)
Feb 26 #Javascript
使用vue-cli编写vue插件的方法
Feb 26 #Javascript
使用ngrok+express解决本地环境中微信接口调试问题
Feb 26 #Javascript
element-ui 表格实现单元格可编辑的示例
Feb 26 #Javascript
element ui里dialog关闭后清除验证条件方法
Feb 26 #Javascript
Vue 中的compile操作方法
Feb 26 #Javascript
You might like
967 个函式
2006/10/09 PHP
php使用高斯算法实现图片的模糊处理功能示例
2016/11/11 PHP
Yii框架弹出框功能示例
2017/01/07 PHP
php如何获取Http请求
2020/04/30 PHP
返回对象在当前级别中是第几个元素的实现代码
2011/01/20 Javascript
纯JS实现五子棋游戏兼容各浏览器(附源码)
2013/04/24 Javascript
node.js中的fs.link方法使用说明
2014/12/15 Javascript
用JS实现轮播图效果(二)
2016/06/26 Javascript
js 获取范围内的随机数实例代码
2016/08/02 Javascript
js select下拉联动 更具级联性!
2020/04/17 Javascript
微信小程序教程系列之新建页面(4)
2017/04/17 Javascript
JS实现判断图片是否加载完成的方法分析
2018/07/31 Javascript
js实现随机点名功能
2020/12/23 Javascript
星球大战与Python之间的那些事
2016/01/07 Python
Python实现字符串与数组相互转换功能示例
2017/09/22 Python
基于循环神经网络(RNN)的古诗生成器
2018/03/26 Python
Python 创建新文件时避免覆盖已有的同名文件的解决方法
2018/11/16 Python
Python3 中作为一等对象的函数解析
2019/12/11 Python
详解KMP算法以及python如何实现
2020/09/18 Python
python 如何将office文件转换为PDF
2020/09/22 Python
python中pdb模块实例用法
2021/01/15 Python
英国办公用品商店:Office Outlet
2018/04/04 全球购物
SNIDEL官网:日本VIVI杂志人气少女第一品牌
2020/03/12 全球购物
Java如何格式化日期
2012/08/07 面试题
大学生学习2014全国两会心得体会
2014/03/13 职场文书
幼儿园的门卫岗位职责
2014/04/10 职场文书
六查六看自查报告
2014/10/14 职场文书
上课迟到检讨书300字
2014/10/15 职场文书
初二学生评语大全
2014/12/26 职场文书
2015年党员个人自我评价
2015/03/03 职场文书
音乐课《小猫钓鱼》教学反思
2016/02/18 职场文书
2019新员工试用期转正申请书3篇
2019/08/13 职场文书
2020年元旦晚会策划书模板
2019/12/30 职场文书
pytorch 实现变分自动编码器的操作
2021/05/24 Python
浅谈Web Storage API的使用
2021/06/23 Javascript
vue实现Toast组件轻提示
2022/04/10 Vue.js