详解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 相关文章推荐
javascript 文档的编码问题解决
Mar 01 Javascript
javascript instanceof 内部机制探析
Oct 15 Javascript
jQuery怎么解析Json字符串(Json格式/Json对象)
Aug 09 Javascript
jquery复选框checkbox实现删除前判断
Apr 20 Javascript
jQuery实现点击按钮文字变成input框点击保存变成文字
May 09 Javascript
AngularJS基础 ng-keydown 指令简单示例
Aug 02 Javascript
JS实现放大、缩小及拖拽图片的方法【可兼容IE、火狐】
Aug 23 Javascript
jquery中用函数来设置css样式
Dec 22 Javascript
js 获取html5的data属性实现方法
Jul 28 Javascript
jQuery实现手势解锁密码特效
Aug 14 jQuery
BootStrap给table表格的每一行添加一个按钮事件
Sep 07 Javascript
vue给对象动态添加属性和值的实例
Sep 09 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
CI框架中zip类应用示例
2014/06/17 PHP
php实现的简易扫雷游戏实例
2015/07/09 PHP
PHP类相关知识点实例总结
2016/09/28 PHP
php str_replace替换指定次数的方法详解
2017/05/05 PHP
PHP简单计算两个时间差的方法示例
2017/06/20 PHP
php ajax confirm 删除实例详解
2019/03/06 PHP
laravel 配置路由 api和web定义的路由的区别详解
2019/09/03 PHP
表单填写时用回车代替TAB的实现方法
2007/10/09 Javascript
jQuery选中select控件 无法设置selected的解决方法
2010/09/01 Javascript
jquery $.ajax相关用法分享
2012/03/16 Javascript
基于jquery编写的横向自适应幻灯片切换特效的实例代码
2013/08/06 Javascript
jQuery学习笔记之jQuery中的$
2015/01/19 Javascript
NodeJS学习笔记之Connect中间件应用实例
2015/01/27 NodeJs
jQuery判断多个input file 都不能为空的例子
2015/06/23 Javascript
jQuery Validation Plugin验证插件手动验证
2016/01/26 Javascript
jQuery插件datatables使用教程
2016/04/21 Javascript
Nodejs下DNS缓存问题浅析
2016/11/16 NodeJs
springMVC + easyui + $.ajaxFileUpload实现文件上传注意事项
2017/04/23 Javascript
JS+canvas实现的五子棋游戏【人机大战版】
2017/07/19 Javascript
详解使用React进行组件库开发
2018/02/06 Javascript
vue中使用iview自定义验证关键词输入框问题及解决方法
2018/03/26 Javascript
Node.js系列之发起get/post请求(2)
2019/08/30 Javascript
JavaScript基于用户照片姓名生成海报
2020/05/29 Javascript
JS寄快递地址智能解析的实现代码
2020/07/16 Javascript
Python实现扫描指定目录下的子目录及文件的方法
2014/07/16 Python
Python使用metaclass实现Singleton模式的方法
2015/05/05 Python
python字符串反转的四种方法详解
2019/12/02 Python
解决Keras中循环使用K.ctc_decode内存不释放的问题
2020/06/29 Python
Python unittest基本使用方法代码实例
2020/06/29 Python
python中类与对象之间的关系详解
2020/12/16 Python
生日宴会答谢词
2014/01/09 职场文书
自荐书4要点
2014/01/25 职场文书
2014年度考核工作总结
2014/12/24 职场文书
开学第一天的感想
2015/08/10 职场文书
《秦兵马俑》教学反思
2016/02/24 职场文书
MySQL选择合适的备份策略和备份工具
2022/06/01 MySQL