Summary
Angular 4 的脏值检测是个老话题了, 而理解这个模型是做 Angular 性能优化的基础. 因此, 今天我们再来聊聊 Angular 4 脏值检测的原理, 并看看性能优化的小提示.
进入点 - Zone.js
Angular 4 是一个 MVVM 框架. 数据模型 (Model) 转换成视图模型 (ViewModel) 后, 绑定到视图 (View) 上渲染成肉眼可见的页面. 因此, 发现数据模型变化的时间点是更新页面的关键, 也是调用脏值检测的关键.
经过分析, 工程师们发现, 数据的变化往往由 macrotask 和 microtask 等异步事件引起. 因此, 通过重写浏览器所有的异步 API, 就能从源头有效地监听数据变化. Zone.js 就是这样一个猴子脚本(Monkey Patch).Angular 4 使用了一个定制化的 Zone(NgZone), 它会通知 Angular 可能有数据变化, 需要更新视图中的数据(脏值检测).
脏值检测(Change Detection)
脏值检测的基本原理是存储旧数值, 并在进行检测时, 把当前时刻的新值和旧值比对. 若相等则没有变化, 反之则检测到变化, 需要更新视图.
Angular 4 把页面切分成若干个 Component(组件), 组成一棵组件树. 进入脏值检测后, 从根组件自顶向下进行检测. Angular 有两种策略: Default 和 OnPush. 它们配置在组件上, 决定脏值检测过程中不同的行为.
Default - 缺省策略
ChangeDetectionStrategy.Default. 它还意味着一旦发生可能有数据变化的事件, 就总是检测这个组件.
脏值检测的操作基本上可以理解为以下几步. 1)更新子组件绑定的 properties,2)调用子组件的 NgDoCheck 和 NgOnChanges 生命周期钩子 (Lifecycle hook),3) 更新自己的 DOM,4)对子组件脏值检测. 这是一个从根组件开始的递归方程.
- // This is not Angular code
- function changeDetection(component) {
- updateProperties(component.children);
- component.children.forEach(child => {
- child.NgDoCheck();
- child.NgOnChanges();
- };
- updateDom(component);
- component.children.forEach(child => changeDetection(child));
- }
我们开发者会非常关注 DOM 更新的顺序, 以及调用 NgDoCheck 和 NgOnChanges 的顺序. 可以发现:
DOM 更新是深度优先的
NgDoCheck 和 NgOnChanges 并不是(也不是深度优先)
OnPush - 单次检测策略
ChangeDetectionStrategy.OnPush. 只在 Input Properties 变化 (OnPush) 时才检测这个组件. 因此当 Input 不变时, 它只在初始化时检测, 也叫单次检测. 它的其他行为和 Default 保持一致.
需要注意的是, OnPush 只检测 Input 的引用. Input 对象的属性变化并不会触发当前组件的脏值检测.
虽然 OnPush 策略提高了性能, 但也是 Bug 的高发地点. 解决方案往往是将 Input 转化成 Immutable 的形式, 强制 Input 的引用改变.
Tips
数据绑定
Angular 有 3 种合法的数据绑定方式, 但它们的性能是不一样的.
直接绑定数据
- <ul>
- <li *ngFor="let item of arr">
- <span>Name {{item.name}}</span>
- <span>Classes {{item.classes}}</span><!-- Binding a data directly. -->
- </li>
- </ul>
大多数情况下, 这都是性能最好的方式.
绑定一个 function 调用结果
- <ul>
- <li *ngFor="let item of arr">
- <span>Name {{item.name}}</span>
- <span>Classes {{classes(item)}}</span><!-- Binding an attribute to a method. The classes would be called in every change detection cycle -->
- </li>
- </ul>
在每个脏值检测过程中, classes 方程都要被调用一遍. 设想用户正在滚动页面, 多个 macrotask 产生, 每个 macrotask 都至少进行一次脏值检测. 如果没有特殊需求, 应尽量避免这种使用方式.
绑定数据 + pipe
- <ul>
- <li *ngFor="let item of instructorList">
- <span>Name {{item.name}}</span>
- <span>Classes {{item | classPipe}}</span><!-- Binding data with a pipe -->
- </li>
- </ul>
它和绑定 function 类似, 每次脏值检测 classPipe 都会被调用. 不过 Angular 给 pipe 做了优化, 加了缓存, 如果 item 和上次相等, 则直接返回结果.
NgFor
多数情况下, NgFor 应该伴随 trackBy 方程使用. 否则, 每次脏值检测过程中, NgFor 会把列表里每一项都执行更新 DOM 操作.
- @Component({
- selector: 'my-app',
- template: `
- <ul>
- <li *ngFor="let item of collection;trackBy: trackByFn">{{item.id}}</li>
- </ul>
- <button (click)="getItems()">Refresh items</button>
- `,
- })
- export class App {
- collection;
- constructor() {
- this.collection = [{id: 1}, {id: 2}, {id: 3}];
- }
- getItems() {
- this.collection = this.getItemsFromServer();
- }
- getItemsFromServer() {
- return [{id: 1}, {id: 2}, {id: 3}, {id: 4}];
- }
- trackByFn(index, item) {
- return index;
- }
- }
- Reference
He who thinks change detection is depth-first and he who thinks it's breadth-first are both usually right https://blog.angularindepth.com/he-who-thinks-change-detection-is-depth-first-and-he-who-thinks-its-breadth-first-are-both-usually-8b6bf24a63e6
Angular Runtime Performance Guide https://blog.oasisdigital.com/2017/angular-runtime-performance-guide/
Photo by Ross Findon https://unsplash.com/photos/mG28olYFgHI?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText on Unsplash https://unsplash.com/search/photos/change?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText
来源: https://juejin.im/post/5adc5fb8518825673b619a2b