简单来讲变化检测就是
Angular
用来检测视图与模型之间绑定的值是否发生了改变,当检测到模型中绑定的值发生改变时,则同步到视图上,反之,当检测到视图上绑定的值发生改变时,则回调对应的绑定函数。javascript
总结起来, 主要有以下几种状况可能也改变数据:java
setTimeout
,setInterval
上述三种状况都有一个共同点,即这些致使绑定值发生改变的事件都是异步发生的。若是这些异步的事件在发生时可以通知到Angular
框架,那么Angular
框架就能及时的检测到变化。web
左边表示将要运行的代码,这里的stack
表示Javascript
的运行栈,而webApi
则是浏览器中提供的一些Javascript
的API
,TaskQueue
表示Javascript
中任务队列,由于Javascript
是单线程的,异步任务在任务队列中执行。promise
具体来讲,异步执行的运行机制以下:浏览器
execution context stack
)。task queue
)。只要异步任务有了运行结果,就在"任务队列"之 中放置一个事件。当上述代码在Javascript
中执行时,首先func1
进入运行栈,func1
执行完毕后,setTimeout
进入运行栈,执行setTimeout
过程当中将回调函数cb
加入到任务队列,而后setTimeout
出栈,接着执行func2
函数,func2
函数执行完毕时,运行栈为空,接着任务队列中cb
进入运行栈获得执行。能够看出异步任务首先会进入任务队列,当运行栈中的同步任务都执行完毕时,异步任务进入运行栈获得执行。若是这些异步的任务执行前与执行后能提供一些钩子函数,经过这些钩子函数,Angular
便能获知异步任务的执行。bash
那么问题来了,angular
是如何知道数据发生了改变?又是如何知道须要修改DOM的位置,准确的最小范围的修改DOM呢?没错,尽量小的范围修改DOM,由于操做DOM对于性能来讲但是一件奢侈品。app
在AngularJS
中是由代码$scope.$apply()
或者$scope.$digest
触发,而Angular
接入了ZoneJS
,由它监听了Angular
全部的异步事件。框架
ZoneJS
是怎么作到的呢?异步
实际上Zone有一个叫猴子补丁的东西。在Zone.js
运行时,就会为这些异步事件作一层代理包裹,也就是说Zone.js运行后,调用setTimeout、addEventListener
等浏览器异步事件时,再也不是调用原生的方法,而是被猴子补丁包装事后的代理方法。代理里setup了钩子函数, 经过这些钩子函数, 能够方便的进入异步任务执行的上下文.async
//如下是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)的引用发生变化或者有事件触发时,组件才进行变化检测。
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
自己的引用,所以是没有发生变化。
而后变化检测进入到叶子节 点MovieComponent:
由于MovieComponent再也没有了叶子节点,因此变化检测将更新DOM,同步视图与模型之间的变化。
而当咱们点击第二个按钮改变明星对象时 ,从新new了一个 star
,这时变化检测才会检测到 star
发生了改变。
而后变化检测进入到子组件中,检测到star.firstName
和star.lastName
发生了变化, 而后更新视图.
与上面代码相比, 只在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()
- 从该组件到各个子组件执行一次变化检测
组件中添加事件改变输入属性
在上面代码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更改以下
第二种就是上面讲到的使用变化检测对象中的 markForCheck()
方法.
ngOnInit() {
setInterval(() => {
this.star.lastName = 'xjl';
this.changeRef.markForCheck();
}, 1000);
}
复制代码
修改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
})
复制代码