什么是变化监测

在使用 Angular 进行开发中,我们常用到 Angular 中的绑定——模型到视图的输入绑定、视图到模型的输出绑定以及视图与模型的双向绑定。而这些绑定的值之所以能在视图与模型之间保持同步,正是得益于Angular中的变化检测。

简单来说,变化检测就是 Angular 用来检测视图与模型之间绑定的值是否发生了改变,当检测到模型中绑定的值发生改变时,则同步到视图上,反之,当检测到视图上绑定的值发生改变时,则回调对应的绑定函数。

变化监测的源头

变化监测的关键在于如何最小粒度地监测到绑定的值是否发生了改变,那么在什么情况下会导致这些绑定的值发生变化呢?我们可以看一下我们常用的几种场景:

Events: click/hover/...

@Component({
selector: 'demo-component',
template: `
<h1>{{name}}</h1>
<button (click)="changeName()">change name</button>
`
})
export class DemoComponent {
name: string = 'Tom'; changeName() {
this.name = 'Jerry';
}
}

我们在模板中通过插值表达式绑定了 name 属性。当点击change name按钮时,改变了 name 属性的值,这时模板视图显示内容也发生了改变。

XHR/webSocket

@Component({
selector: 'demo-component',
template: `
<h1>{{name}}</h1>
`
})
export class DemoComponent implements OnInit {
name: string = 'Tom'; constructor(public http: HttpClient) {} ngOnInit() {
// 假设有这个./getNewName请求,返回一个新值'Jerry'
this.http.get('./getNewName').subscribe((data: string) => {
this.name = data;
});
}
}

我们在这个组件的 ngOnInit 函数里向服务器端发送了一个 Ajax 请求,当这个请求返回结果时,同样会改变当前模板视图上绑定的 name 属性的值。

Times: setTimeout/requestAnimationFrame

@Component({
selector: 'demo-component',
template: `
<h1>{{name}}</h1>
`
})
export class DemoComponent implements OnInit {
name: string = 'Tom'; constructor() {} ngOnInit() {
// 假设有这个./getNewName请求,返回一个新值'Jerry'
setTimeout(() => {
this.name = 'Jerry';
}, 1000);
}
}

我们在这个组件的ngOnInit函数里通过设定一个定时任务,当定时任务执行时,同样会改变当前视图上绑定的name属性的值。

总结

  • 其实,我们不难发现上述三种情况都有一个共同点,即这些导致绑定值发生改变的事件都是异步发生的。

  • Angular并不是捕捉对象的变动,它采用的是在适当的时机去检验对象的值是否被改动,这个时机就是这些异步事件的发生。

  • 这个时机是由 NgZone 这个服务去掌控的,它获取到了整个应用的执行上下文,能够对相关的异步事件发生、完成或者异常等进行捕获,然后驱动 Angular 的变化监测机制执行。

变化监测的处理机制

通过上面的介绍,我们大致明白了变化检测是如何被触发的,那么 Angular 中的变化监测是如何执行的呢?

首先我们需要知道的是,对于每一个组件,都有一个对应的变化监测器;即每一个 Component 都对应有一个changeDetector,我们可以在 Component 中通过依赖注入来获取到changeDetector

而我们的多个 Component 是一个树状结构的组织,由于一个 Component 对应一个changeDetector,那么changeDetector之间同样是一个树状结构的组织。

最后我们需要记住的一点是,每次变化监测都是从 Component 树根开始的。

举个例子

子组件:

@Component({
selector: 'demo-child',
template: `
<h1>{{title}}</h1>
<p>{{paramOne}}</p>
<p>{{paramTwo}}</p>
`
})
export class DemoChildComponent {
title: string = '子组件标题';
@Input() paramOne: any; // 输入属性1
@Input() paramTwo: any; // 输入属性2
}

父组件:

@Component({
selector: 'demo-parent',
template: `
<h1>{{title}}</h1>
<demo-child [paramOne]='paramOneVal' [paramTwo]='paramTwoVal'></demo-child>
<button (click)="changeVal()">change name</button>
`
})
export class DemoParentComponent {
title: string = '父组件标题';
paramOneVal: any = '传递给paramOne的数据';
paramTwoVal: any = '传递给paramTwo的数据'; changeVal() {
this.paramOneVal = '改变之后的传递给paramOne的数据';
}
}

上面的代码中,DemoParentComponent 通过 标签嵌入了 DemoChildComponent,从树状结构上来说,DemoParentComponent 是 DemoChildComponent 的根节点,而 DemoChildComponent 是 DemoParentComponent 的叶子节点。

当我们点击 DemoParentComponent 的 button 时,会回调到 changeVal 方法,然后会触发变化监测的执行,变化监测流程如下:

首先变化检测从 DemoParentComponent 开始:

  • 检测 title 值是否发生了变化:没有发生变化

  • 检测 paramOneVal 值是否发生了变化:发生了变化(点击按钮调用changeVal()方法改变的)

  • 检测 paramTwoVal 值是否发生了变化:没有发生变化

然后变化检测进入到叶子节点 DemoChildComponent:

  • 检测 title 值是否发生了改变:没有发生变化

  • 检测 paramOne 是否发生了变化:发生了改变(由于父组件的属性paramOneVal发生了改变)

  • 检测 paramTwo 是否发生了改变:没有发生变化

最后,因为 DemoChildComponent 再也没有了叶子节点,所以变化监测将更新DOM,同步视图与模型之间的变化。

变化监测策略

学习了变化监测的处理机制之后,你可能会想,这机制未免也有点太简单粗暴了吧,假如我的应用中有成百上千个 Component,随便一个 Component 触发了监测,那么都需要从根节点到叶子节点重新检测一遍。

别着急,Angular 的开发团队已经考虑到了这个问题,上述的检测机制只是一种默认的检测机制,Angular 还提供一种 OnPush 的检测机制(设置元数据属性 changeDetection: ChangeDetectionStrategy.OnPush)。

OnPush 与 Default 之间的差别:当检测到与子组件输入绑定的值没有发生改变时,变化检测就不会深入到子组件中去

变化监测类 - ChangeDetectorRef

上面说到我们可以修改组件元数据属性 changeDetection 来修改组件的变化监测策略(ChangeDetectionStrategy.Default 或 ChangeDetectionStrategy.OnPush),除了这个,我们还可以使用 ChangeDetectorRef 来更加灵活的控制组件的变化监测。

Angular 在整个运行期间都会为每一个组件创建 ChangeDetectorRef 的实例,该实例提供了相关方法来手动管理变化监测。有了这个类,我们自己就可以自定义组件的变化监测策略了,如停止/启用变化监测或者按指定路径变化监测等等。

相关方法如下:

  • markForCheck():把根组件到该组件之间的这条路径标记起来,通知Angular在下次触发变化监测时必须检查这条路径上的组件。

  • detach():从变化监测树中分离变化监测器,该组件的变化监测器将不再执行变化监测,除非再次手动执行reattach()方法。

  • reattach():把分离的变化监测器重新安装上,使得该组件及其子组件都能执行变化监测。

  • detectChanges():手动触发执行该组件到各个子组件的一次变化监测。

使用方法也很简单,直接在组件中注入即可:

@Component({
selector: 'demo-parent',
template: `
<h1>{{title}}</h1>
`
})
export class DemoParentComponent implements OnInit {
title: string = '组件标题'; constructor(public cdRef: ChangeDetectorRef) {} ngOnInit() {
this.cdRef.detach(); // 停止组件的变化监测,看需求使用不同的方法
}
}

Angular开发实践(五):深入解析变化监测的更多相关文章

  1. Angular开发实践(一):环境准备及框架搭建

    引言 在工作中引入Angular框架将近一年了,在这一年中不断的踩坑和填坑,当然也学习和积累了很多的知识,包括MVVM框架.前后端分离.前端工程化.SPA优化等等.因此想通过Angular开发实践这系 ...

  2. Angular开发实践(七): 跨平台操作DOM及渲染器Renderer2

    在<Angular开发实践(六):服务端渲染>这篇文章的最后,我们也提到了在服务端渲染中需要牢记的几件事件,其中就包括不要使用window. document. navigator等浏览器 ...

  3. nodejs 实践:express 最佳实践(五) connect解析

    nodejs 实践:express 最佳实践(五) connect解析 nodejs 发展很快,从 npm 上面的包托管数量就可以看出来.不过从另一方面来看,也是反映了 nodejs 的基础不稳固,需 ...

  4. Angular开发实践(三):剖析Angular Component

    Web Component 在介绍Angular Component之前,我们先简单了解下W3C Web Components 定义 W3C为统一组件化标准方式,提出Web Component的标准. ...

  5. Angular开发实践(六):服务端渲染

    Angular Universal Angular在服务端渲染方面提供一套前后端同构解决方案,它就是 Angular Universal(统一平台),一项在服务端运行 Angular 应用的技术. 标 ...

  6. Angular开发实践(四):组件之间的交互

    在Angular应用开发中,组件可以说是随处可见的.本篇文章将介绍几种常见的组件通讯场景,也就是让两个或多个组件之间交互的方法. 根据数据的传递方向,分为父组件向子组件传递.子组件向父组件传递及通过服 ...

  7. Angular开发实践(八): 使用ng-content进行组件内容投射

    在Angular中,组件属于特殊的指令,它的特殊之处在于它有自己的模板(html)和样式(css).因此使用组件可以使我们的代码具有强解耦.可复用.易扩展等特性.通常的组件定义如下: demo.com ...

  8. httprunner开发实践&源码解析

    上次作业讲解 排错 控制台查看报错信息 打开代理工具,调试脚本 注释掉其他接口,先跑一个接口 pip uninstall httprunner 修复断言100为int型问题 修复两次登陆问题 报告 p ...

  9. ionic,Angular 开发实践

    1.实践参考 http://www.jianshu.com/p/ea0dcf1d31c9 原文思路搭建 2. 环境搭建步骤 : a. 安装node b.安装 cordova      sudo   n ...

随机推荐

  1. 【linux】安装mysql出现 no curses错误解决方法

    安装mysql,在./configure时出现错误:error: No curses/termcap library found的解决办法 mysql版本:5.1.30 已经不记得这次是第几次安装my ...

  2. HighCharts之气泡图

    HighCharts之气泡图 1.HighCharts之气泡图源码 bubble.html: <!DOCTYPE html> <html> <head> <m ...

  3. oracle分析函数技术详解(配上开窗函数over())

    一.Oracle分析函数入门 分析函数是什么?分析函数是Oracle专门用于解决复杂报表统计需求的功能强大的函数,它可以在数据中进行分组然后计算基于组的某种统计值,并且每一组的每一行都可以返回一个统计 ...

  4. UEFI模式 Thinkpad t470p Ubuntu 16.04 LTS

    准备阶段 使用官方推荐的Rufus制作U盘启动盘 在Windows 10系统下压缩出来一些空间(60G),不要分配盘符 系统设置 在Bios中关闭secure boot (设置为Disenabled) ...

  5. freemarker获取封装类中对象的属性(六)

    freemarker获取封装类中对象的属性 1.设计思路 (1)封装学生类 (2)创建数据模型 (3)新建student.ftl (4)运行Junit测试文件,生成HTML文件 2.封装学生类 Stu ...

  6. MySQL新建用户与授权

    一.登录root用户 [root@iZm5e9gg1p5y1co0usac2cZ webapps] # mysql -uroot -p 二.新建用户 mysql> create user '; ...

  7. 洛谷P3459 [POI2007]MEG-Megalopolis(树链剖分,Splay)

    洛谷题目传送门 正解是树状数组维护dfn序上的前缀和,这样的思路真是又玄学又令我惊叹( 我太弱啦,根本想不到)Orz各路Dalao 今天考了这道题,数据范围还比洛谷的小,只有\(10^5\)(害我复制 ...

  8. 浅谈API安全设计

    一.简述 安全是恒久的话题,如果不注意防范,会带来很严重的后果.比如: 1.接口被大规模调用消耗系统资源,影响系统的正常访问,甚至系统瘫痪 2.数据泄露 3.伪造(篡改)数据,制造垃圾数据 4.App ...

  9. JAVA线程sleep和wait方法区别

    一. sleep 是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复,调用sleep 不会释放对象锁.由于没有释放对象锁,所以不能 ...

  10. Solidity调试 - 实现变量打印

    Solidity没有print或console.log方法可以用来打印变量,这会给我们调试程序增加难度. Solidity有event功能,可以在event中记录变量信息,通过调用event方法也可以 ...