Angular系列之变化检测(ChangeDetection)
概述
简单来说变化检测就是Angular⽤来检测视图与模型之间绑定的值是否发⽣了改变,当检测到模型中绑定的值发⽣改变时,则同步到视图上,反之,当检测到视图上绑定的值发⽣改变时,则回调对应的绑定函数。
什么情况下会引起变化检测?
总结起来, 主要有如下⼏种情况可能也改变数据:
⽤户输⼊操作,⽐如点击,提交等
请求服务端数据(XHR)
定时事件,⽐如setTimeout,setInterval
上述三种情况都有⼀个共同点,即这些导致绑定值发⽣改变的事件都是异步发⽣的。如果这些异步的事件在发⽣时能够通知到Angular框架,那么Angular框架就能及时的检测到变化。
左边表⽰将要运⾏的代码,这⾥的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;
veEventListener=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 策略
mainponent.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('刘', '德华');
}
}
movieponent.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策略
与上⾯代码相⽐, 只在movieponent.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.OnP
ush 条件,那么变化检测不会再次执⾏,除⾮⼿动调⽤该⽅法, 该⽅法的意思是在变化监测时必须检测该组件。
detach() - 从变化检测树中分离变化检测器,该组件的变化检测器将不再执⾏变化检测,除⾮⼿动调⽤ reattach() ⽅法。
reattach() - 重新添加已分离的变化检测器,使得该组件及其⼦组件都能执⾏变化检测
detectChanges() - 从该组件到各个⼦组件执⾏⼀次变化检测
OnPush策略下⼿动发起变化检测
组件中添加事件改变输⼊属性
在上⾯代码movieponent.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';
}angular和angularjs
}
此时点击按钮切换名字时,star更改如下
!
[图⽚描述][3]
第⼆种就是上⾯讲到的使⽤变化检测对象中的 markForCheck()⽅法.
ngOnInit() {
setInterval(() => {
this.star.lastName = 'xjl';
this.changeRef.markForCheck();
}, 1000);
}
输⼊属性为Observable
修改appponent.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(){
}
changeStar() {
this.star.firstName = '吴';
this.star.lastName = '彦祖';
}
changeStarObject() {
this.star = new Star('刘', '德华');
}
}
此时,有两种⽅式让MovieComponent进⼊检测,⼀种是使⽤变化检测对象中的 markForCheck()⽅法.
ngOnInit() {
this.addCount.subscribe(() => {
this.changeRef.markForCheck();
})
另外⼀种是使⽤async pipe 管道
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论