详解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 命名空间以提高代码重用性
Nov 13 Javascript
js函数在frame中的相互调用详解
Mar 03 Javascript
javascript判断是手机还是电脑访问网页的简单实例分享
Jun 03 Javascript
把多个JavaScript函数绑定到onload事件处理函数上的方法
Sep 04 Javascript
Bootstrap文件上传组件之bootstrap fileinput
Nov 25 Javascript
基于JavaScript实现全选、不选和反选效果
Feb 15 Javascript
Angular2仿照微信UI实现9张图片上传和预览的示例代码
Oct 19 Javascript
js中数组常用方法总结(推荐)
Apr 09 Javascript
layui 实现table翻页滚动条位置保持不变的例子
Sep 05 Javascript
JS实现点星星消除小游戏
Mar 24 Javascript
vue keep-alive实现多组件嵌套中个别组件存活不销毁的操作
Oct 30 Javascript
vue前端和Django后端如何查询一定时间段内的数据
Feb 28 Vue.js
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
浅析PHP递归函数返回值使用方法
2013/02/18 PHP
ThinkPHP使用心得分享-上传类UploadFile的使用
2014/05/15 PHP
php生成百度sitemap站点地图类函数实例
2014/10/17 PHP
php操作access数据库的方法详解
2017/02/22 PHP
jQuery学习笔记 操作jQuery对象 CSS处理
2012/09/19 Javascript
jquery Tab效果和动态加载的简单实例
2013/12/11 Javascript
JQuery1.8 判断元素是否绑定事件的方法
2014/07/10 Javascript
javascript中数组的定义及使用实例
2015/01/21 Javascript
javascript 常见功能汇总
2015/06/11 Javascript
Javascript常用小技巧汇总
2015/06/24 Javascript
基于JavaScript实现仿京东图片轮播效果
2015/11/06 Javascript
浅析JavaScript访问对象属性和方法及区别
2015/11/16 Javascript
Bootstrap实现弹性搜索框
2016/07/11 Javascript
node+experss实现爬取电影天堂爬虫
2016/11/20 Javascript
js实现抽奖效果
2017/03/27 Javascript
在vue中获取dom元素内容的方法
2017/07/10 Javascript
详解微信小程序开发之formId使用(模板消息)
2019/08/27 Javascript
Vue中的循环及修改差值表达式的方法
2019/08/29 Javascript
js中addEventListener()与removeEventListener()用法案例分析
2020/03/02 Javascript
three.js着色器材质的内置变量示例详解
2020/08/16 Javascript
OpenLayers3加载常用控件使用方法详解
2020/09/25 Javascript
利用JS判断元素是否为数组的方法示例
2021/01/08 Javascript
[02:02:38]VG vs Mineski Supermajor 败者组 BO3 第一场 6.6
2018/06/07 DOTA
[01:16:12]完美世界DOTA2联赛PWL S2 FTD vs Inki 第一场 11.21
2020/11/23 DOTA
Python PyQt5标准对话框用法示例
2017/08/23 Python
python实现机器学习之元线性回归
2018/09/06 Python
python函数声明和调用定义及原理详解
2019/12/02 Python
Keras中的两种模型:Sequential和Model用法
2020/06/27 Python
Html5基于canvas实现电子签名并生成PDF文档
2020/12/07 HTML / CSS
导游个人求职信范文
2014/03/23 职场文书
《彩色世界》教学反思
2014/04/12 职场文书
三年级评语大全
2014/04/23 职场文书
营销团队口号
2014/06/06 职场文书
电子专业自荐信
2014/07/01 职场文书
机关作风建设工作总结
2014/10/23 职场文书
公司规章制度范本
2015/08/03 职场文书