阅读 Angular 6/RxJS 最新教程,请访问 前端修仙之路
Change Detection (变化检测) 是 Angular 2 中最重要的一个特性。当组件中的数据发生变化的时候,Angular 2 能检测到数据变化并自动刷新视图反映出相应的变化。javascript
在介绍变化检测以前,咱们要先介绍一下浏览器中渲染的概念,渲染是将模型映射到视图的过程。模型的值能够是 JavaScript 中的原始数据类型、对象、数组或其余数据对象。然而视图能够是页面中的段落、表单、按钮等其余元素,这些页面元素内部使用 DOM (Document Object Model) 来表示。css
为了更好地理解,咱们来看一个具体的示例:html
<h4 id="greeting"></h4> <script> document.getElementById("greeting").innerHTML = "Hello World!"; </script>
这个例子很简单,由于模型不会变化,因此页面只会渲染一次。若是数据模型在运行时会不断变化,那么整个过程将变得复杂。所以为了保证数据与视图的同步,页面将会进行屡次渲染。接下来咱们来考虑一下如下几个问题:前端
而变化检测的基本目的就是解决上述问题。在 Angular 2 中当组件内的模型发生变化的时候,组件内的变化检测器就会检测到更新,而后通知视图刷新。所以变化检测器有两个主要的任务:java
接下来咱们来分析一下什么是变化,变化是怎么产生的。git
变化是旧模型与新模型之间的区别,换句话说变化产生了一个新的模型。让咱们来看一下下面的代码:github
import { Component } from '@angular/core'; @Component({ selector: 'exe-counter', template: ` <p>当前值:{{ counter }}</p> <button (click)="countUp()"> + </button>` }) export class CounterComponent { counter = 0; countUp() { this.counter++; } }
页面首次渲染完后,计数器的当前值为0。当咱们点击 +
按钮时,计数器的 counter 值将会自动加1,以后页面中当前值也会被更新。在这个例子中,点击事件引发了 counter 属性值的变化。web
咱们继续看下一个例子:typescript
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'exe-counter', template: ` <p>当前值:{{ counter }}</p> ` }) export class CounterComponent implements OnInit { counter = 0; ngOnInit() { setInterval(() => { this.counter++; }, 1000); } }
该组件经过 setInterval
定时器,实现每秒钟 counter
值自动加1。在这种状况下,它是定时器事件引发了属性值的变化。最后咱们再来看个例子:shell
import { Component, OnInit } from '@angular/core'; import { Http } from '@angular/http'; @Component({ selector: 'exe-counter', template: ` <p>当前值:{{ counter }}</p> ` }) export class CounterComponent implements OnInit { counter = 0; constructor(private http: Http) {} ngOnInit() { this.http.get('/counter-data.json') .map(res => res.json()) .subscribe(data => { this.counter = data.value; }); } }
该组件在进行初始化的时候,会发送一个 HTTP
请求去获取初始值。当请求成功返回的时候,组件的 counter 属性的值会被更新。在这种状况下,它是由 XHR 回调引发了属性值的变化。
如今咱们来总结一下,引发模型变化的三类事件源:
这些事件源有一个共同的特性,即它们都是异步操做。那咱们能够这样认为,全部的异步操做都有可能会引发模型的变化。
很是好,你已经了解了引发模型变化的事件源和触发变化的时机点。可是你还不知道,是由谁来负责通知相应的变化给视图。接下来,咱们将讨论一种容许 Angular 随时检测到变化的机制,它被称为 Zone
。
Zone 是下一个 ECMAScript 规范的建议之一。Angular 团队实现了 JavaScript 版本的 zone.js ,它是用于拦截和跟踪异步工做的机制。
Zone 是一个全局的对象,用来配置有关如何拦截和跟踪异步回调的规则。Zone 有如下能力:
咱们来看一个简单的示例:
Zone.current.fork({}).run(function () { Zone.current.inTheZone = true; setTimeout(function () { console.log('in the zone: ' + !!Zone.current.inTheZone); }, 0); }); console.log('in the zone: ' + !!Zone.current.inTheZone);
以上代码运行后的结果是:
in the zone: false in the zone: true
是否是感受很神奇!在Angular 2 中,有一个 NgZone,它是专门为 Angular 2 定制的 zone。在正式介绍它以前,咱们先来看一下 Angular 1.x 中的一个例子:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Angular 1.x Demo</title> <script src="//cdn.bootcss.com/angular.js/1.6.3/angular.min.js"></script> </head> <body ng-app="exeApp"> <div ng-controller="MainCtrl"> <h4>Hello {{ name }}</h4> </div> <script type="text/javascript"> angular.module('exeApp', []) .controller('MainCtrl', ['$scope', function ($scope) { $scope.name = 'Angular'; setTimeout(function () { $scope.name = 'Angular 2'; }, 2000); }]); </script> </body> </html>
以上代码运行后的输出结果:
用过 Angular 1.x 的同窗,应该很清楚能够经过 Angular 1.x 中的 $timeout
服务或手动调用 $scope.$digest()
方法来通知视图刷新。这对初学者来讲,是很麻烦的一件事情。大家应该还记得前面计数器组件的例子,咱们经过 setInterval
定时器,实现每秒钟 counter
值自动加1,页面就自动刷新了。不须要再使用 Angular 1.x 中的 $timeout
服务或手动调用 $scope.$digest()
方法来刷新视图。
为何咱们都是使用定时器,而在 Angular 2 中模型发生变化后,却能自动通知视图进行刷新呢 ?咱们来分析一下,首先在浏览器中新开一个 Tab 页,在控制台输入:
window.setTimeout.toString() "function setTimeout() { [native code] }"
而后再打开一个 Angular 2 应用的页面,在控制台一样输入:
window.setTimeout.toString() "function setTimeout(){return f(this, arguments)}"
咱们发如今 Angular 2 中,setTimeout 方法已经被重写了,最简单的实现方式以下:
var originSetTimeout = window.setTimeout; window.setTimeout = function(fn, delay) { console.log('setTimeout has been called'); originSetTimeout(fn, delay); }
其实在 Angular 2 应用程序启动以前,Zone 采用猴子补丁 (Monkey-patched) 的方式,将 JavaScript 中的异步任务都进行了包装,这使得这些异步任务都能运行在 Zone 的执行上下文中,每一个异步任务在 Zone 中都是一个任务,除了提供了一些供开发者使用的钩子外,默认状况下 Zone 重写了如下方法:
Zone 内部源码片断:
var set = 'set'; var clear = 'clear'; var blockingMethods = ['alert', 'prompt', 'confirm']; var _global = typeof window === 'object' && window || typeof self === 'object' && self || global; patchTimer(_global, set, clear, 'Timeout'); patchTimer(_global, set, clear, 'Interval'); patchTimer(_global, set, clear, 'Immediate'); patchTimer(_global, 'request', 'cancel', 'AnimationFrame'); patchTimer(_global, 'mozRequest', 'mozCancel', 'AnimationFrame'); patchTimer(_global, 'webkitRequest', 'webkitCancel', 'AnimationFrame');
NgZone 是基于 Zone 实现的,它是Zone派生出来的一个子Zone,在 Angular 环境内注册的异步事件都运行在这个子 Zone 内 (由于NgZone拥有整个运行环境的执行上下文),它扩展了自有的一些 API 并添加了一些功能性的方法到它的执行上下文中。
在 Angular 源码中,有一个 ApplicationRef_
类,其做用是用来监听 NgZone 中的 onMicrotaskEmpty
事件,不管什么时候只要触发这个事件,那么将会执行一个 tick
方法用来告诉 Angular 去执行变化检测,简化版的代码以下:
class ApplicationRef { private _views: InternalViewRef[] = []; constructor(private zone: NgZone) { this.zone.onMicrotaskEmpty.subscribe(() => { this.zone.run(() => { this.tick(); }); }); } tick() { if (this._runningTick) { throw new Error('ApplicationRef.tick is called recursively'); } this._views.forEach((view) => view.detectChanges()); } }
如今咱们先来总结一下前面所讲的内容:
要彻底理解 Zone 的工做原理是比较困难的,对咱们大部分的人来讲,只要知道 Angular 内部是经过它来跟踪异步任务,而后执行变化检测任务就能够了。
1.在 Angular 2 项目中怎么访问 Zone 打补丁前的方法,如 setTimeout、clearTimeout 等
由于 Zone 内部经过内建的 __symbol__
函数来模拟 Symbol :
function __symbol__(name) { return '__zone_symbol__' + name; }
所以咱们能够在浏览器的控制台中运行:
Object.keys(window).forEach((key) => { if(key.indexOf('zone_symbol') > 0) { console.log(key); } });
运行后控制台的输出结果以下:
2.前面介绍 Zone 使用的示例,为何控制台会输出那样的结果 ?
// 加载Zone.js给浏览器中的一些异步操做打上补丁 // 建立Root Zone // 调用Zone.current对象上的fork方法建立新的zone,咱们称之为childZone Zone.current.fork({}).run(function () { // 运行run方法,Zone.current被设置为函数被执行时所属的Zone,即childZone Zone.current.inTheZone = true; // 这里注册了一个定时器。因为被打过了猴子补丁,这里调用的并非 // 浏览器"默认"的setTimeout方法。所以,这里其实是在配置代理。这里 // 要重点指出的是这个代理会保留一个指向建立时所属Zone的引用即childZone, // 稍后会用到这个引用。 setTimeout(function () { // 定时时间到,此时的Zone.current的值会被重置为childZone console.log('in the zone: ' + !!Zone.current.inTheZone); }, 0); // 代码执行完 Zone.current属性被重置为Root Zone // Zone的生命周期里的钩子函数会被触发 }); console.log('in the zone: ' + !!Zone.current.inTheZone);
若是仍是很差理解的话,咱们能够想象一下同步的过程:
const rootZone = Zone.current; // 建立一个新的Zone const childZone = Zone.current.fork({}); // 设置当前的zone Zone.current = zone; // 为当前的zone添加inTheZone属性 Zone.current.inTheZone = true; console.log('in the zone: ' + !!Zone.current.inTheZone); // 退出当前的zone Zone.current = rootZone; console.log('in the zone: ' + !!Zone.current.inTheZone);
这篇文章咱们先介绍了浏览器中渲染的概念,而后经过三个示例引出了引发模型变化的事件源并总结了它们之间的共性,此外咱们还介绍了 Angular 1.x 项目中初学者容易遇到的问题,并基于该问题引入了 Zone 和 NgZone 的概念,最后咱们简单介绍了 Zone.js 的内部工做原理。下一篇文章咱们将详细介绍 Angular 2 组件中的变化检测器。