原文连接:Angular.js’ $digest is reborn in the newer version of Angularhtml
我使用 Angular.js 框架好些年了,尽管它饱受批评,但我依然以为它是个难以想象的框架。我是从这本书 Building your own Angular.js 开始学习的,而且读了框架的大量源码,因此我以为本身对 Angular.js 内部机制比较了解,而且对建立这个框架的架构思想也比较熟悉。最近我在试图掌握新版 Angular 框架内部架构思想,并与旧版 Angular.js 内部架构思想进行比较。我发现并非像网上说的那样,偏偏相反,Angular 大量借鉴了 Angular.js 的设计思想。前端
其中之一就是名声糟糕的 digest loop:node
这个设计的主要问题就是成本过高。改变程序中的任何事物,须要执行成百上千个函数去查询哪一个数据发生变化。而这是 Angular 的基础部分,可是它会把查询限定在部分 UI 上,从而提升性能。git
若是能更好理解 Angular 是如何实现 digest 的,就可能把你的程序设计的更高效,好比,使用 $scope.$digest()
而不是 $scope.$apply
,或者使用不可变对象。但事实是,为了设计出更高效的程序,从而去理解框架内部实现,这可能对不少人来讲不是简单的事情。angularjs
因此大量有关 Angular 的文章教程里都宣称框架里不会再有 $digest cycle
了。这取决于对 digest 概念如何理解,但我认为这颇有误导性,由于它仍然存在。的确,在 Angular 里没有 scopes 和 watchers
,也再也不须要调用 $scope.$digest()
,可是检测数据变化的机制依然是遍历整个组件树,隐式调用 watchers
,而后更新 DOM。因此其实是彻底重写了,但被优化加强了,关于新的查询机制能够查看我写的 Everything you need to know about change detection in Angular。github
开始前让咱们先回忆下 Angular.js 中为什么存在 digest
。全部框架都是在解决数据模型(JavaScript Objects)和 UI(Browser DOM)的同步问题,最大难题是如何知道何时数据模型发生改变,而查询数据模型什么时候发生改变的过程就是变动检测(change detection)。这个问题的不一样实现方案也是如今众多前端框架的最大区别点。我计划写篇文章,有关不一样框架变动检测实现的比较,若是你感兴趣并但愿收到通知,能够关注我。api
有两种方式来检测变化:须要使用者通知框架;经过比较来自动检测变化。bash
假设咱们有以下一个对象:前端框架
let person = {name: 'Angular'};
复制代码
而后咱们去更新 name
属性值,可是框架是怎么知道这个值什么时候被更新呢?一种方式是须要使用者告诉框架(注:如 React 方式):angular2
constructor() {
let person = {name: 'Angular'};
this.state = person;
}
...
// explicitly notifying React about the changes
// and specifying what is about to change
this.setState({name: 'Changed'});
复制代码
或者强迫用户去封装该属性,从而框架能添加 setters
(注:如 Vue 方式):
let app = new Vue({
data: {
name: 'Hello Vue!'
}
});
// the setter is triggered so Vue knows what changed
app.name = 'Changed';
复制代码
另外一种方式是保存 name
属性的上一个值,并与当前值进行比较:
if (previousValue !== person.name) // change detected, update DOM
复制代码
可是何时结束比较呢?咱们应该在每一次异步代码运行时都去检查,因为这部分运行的代码是做为异步事件去处理,即所谓的 Virtual Machine(VM) turn/tick(注:Virtual Machine 的理解可参考 VM),因此能够紧接着在 VM turn 的后面,执行数据变化检查代码。这也是为什么 Angular.js 使用 digest
,因此咱们能够定义 digest
为(注:为清晰理解,不翻译):
a change detection mechanism that walks the tree of components, checks each component for changes and updates DOM when a component property is changed。
若是咱们这么去定义 digest
的话,那我能够说数据变化检查机制的主要部分在 Angular 里没有变化,变化的是 digest
的实现。
Angular.js 使用 watcher
和 listener
的概念,watcher
就是一个返回被监测值的函数,大多数时候这个被监测值就是数据模型的属性。但也不老是数据模型属性,如咱们能够在做用域里追踪组件状态,计算属性值,第三方组件等等。若是当前返回值与先前值不一样,Angular.js 就会调用 listener
,而 listener
一般用来更新 UI。
$watch
函数的参数列表以下:
$watch(watcher, listener);
复制代码
因此,若是咱们有一个带有name
属性的 person
对象,并在模板里这样使用 <span>{{name}}</span>
,那就能够像这样去追踪这个属性变化从而更新 DOM:
$watch(() => {
return person.name
}, (value) => {
span.textContent = value
});
复制代码
这与插值和 ng-bind
类的指令本质上作的同样,Angular.js 使用指令来映射 DOM 的数据模型。可是 Angular 再也不这么去作,它使用属性映射来链接数据模型和 DOM。上面的示例在 Angular 会这么实现:
<span [textContent]="person.name"></span>
复制代码
因为存在不少组件,并组成了组件树,每个组件都有着不一样的数据模型,因此就存在分层的 watchers
,与分层的组件树很类似。尽管使用做用域把 watchers
组合在一块儿,但它们并不相关。
如今,在 digest
期间,Angular.js 会遍历 watchers
树并更新 DOM。若是你使用 $timeout
,$http
或根据须要使用 $scope.$apply
和 $scope.$digest
等方式,就会在每一次异步事件中触发 digest cycle
。
watchers
是严格按照顺序触发:首先是父组件,而后是子组件。这颇有意义,但却有着不受欢迎的缺点。一个被触发的 watcher listener
有不少反作用,好比包括更新父组件的属性。若是父监听器已经被触发了,而后子监听器又去更新父组件属性,那这个变化不会被检测到。这就是为什么 digest loop
要运行屡次来获取稳定的程序状态,即确保没有数据再发生变化。运行次数最大限定为 10 次,这个设计如今被认为是有缺陷的,而且 Angular 不允许这样作。
Angular 并无相似 Angular.js 中 watcher
概念,可是追踪模型属性的函数依然存在。这些函数是由框架编译器生成的,而且是私有不可访问的。另外,它们也和 DOM 紧密耦合在一块儿,这些函数就存储在生成视图结构 ViewDefinition 的 updateRenderer 中。
它们也很特别:只追踪模型变化,而不是像 Angular.js 追踪一切数据变化。每个组件都有一个 watcher
来追踪在模板中使用的组件属性,并对每个被监听的属性调用 checkAndUpdateTextInline 函数。这个函数会比较属性的上一个值与当前值,若是有变化就更新 DOM。
好比,AppComponent
组件的模板:
<h1>Hello {{model.name}}</h1>
复制代码
Angular Compiler 会生成以下相似代码:
function View_AppComponent_0(l) {
// jit_viewDef2 is `viewDef` constructor
return jit_viewDef2(0,
// array of nodes generated from the template
// first node for `h1` element
// second node is textNode for `Hello {{model.name}}`
[
jit_elementDef3(...),
jit_textDef4(...)
],
...
// updateRenderer function similar to a watcher
function (ck, v) {
var co = v.component;
// gets current value for the component `name` property
var currVal_0 = co.model.name;
// calls CheckAndUpdateNode function passing
// currentView and node index (1) which uses
// interpolated `currVal_0` value
ck(v, 1, 0, currVal_0);
});
}
复制代码
注:使用 Angular-CLI
ng new
一个新项目,执行ng serve
运行程序后,就可在 Chrome Dev Tools 的 Source Tab 的ng://
域下查看到编译组件后生成的**.ngfactory.js
文件,即上面相似代码。
因此,即便 watcher
实现方式不一样,但 digest loop
仍然存在,仅仅是换了名字为 change detection cycle (注: 为清晰理解,不翻译):
In development mode, tick() also performs a second change detection cycle to ensure that no further changes are detected.
上文说到在 digest
期间,Angular.js 会遍历 watchers
树并更新 DOM,这与 Angular 中机制很是相似。在变动检测循环期间(注:与本文中 digest cycle
相同概念),Angular 也会遍历组件树并调用渲染函数更新 DOM。这个过程是 checking and updating view process 过程的一部分,我也写了一篇长文 Everything you need to know about change detection in Angular 。
就像 Angular.js 同样,在 Angular 中变动检测也一样是由异步事件触发(注:如异步请求数据返回事件;用户点击按钮事件;setTimeout/setInterval
)。可是因为 Angular 使用 zone 包来给全部异步事件打补丁,因此对于大部分异步事件来讲,不须要手动触发变动检测。Angular 框架会订阅 onMicrotaskEmpty 事件,并在一个异步事件完成时会通知 Angular 框架,而这个 onMicrotaskEmpty 事件是在当前 VM Turn 的 microtasks
队列里不存在任务时被触发。然而,变动检测也能够手动方式触发,如使用 view.detectChanges 或 ApplicationRef.tick (注:view.detectChanges
会触发当前组件及子组件的变动检测,ApplicationRef.tick
会触发整个组件树即全部组件的变动检测)。
Angular 强调所谓的单向数据流,从顶部流向底部。在父组件完成变动检测后,低层级里的组件,即子组件,不允许改变父组件的属性。但若是一个组件在 DoCheck 生命周期钩子里改变父组件属性,倒是能够的,由于这个钩子函数是在**更新父组件属性变化以前调用的**(注:即第 6 步 DoCheck, 在 第 9 步 updates DOM interpolations for the current view if properties on current view component instance changed 以前调用)。可是,若是改变父组件属性是在其余阶段,好比 AfterViewChecked 钩子函数阶段,在父组件已经完成变动检测后,再去调用这个钩子函数,在开发者模式下框架会抛出错误:
Expression has changed after it was checked
关于这个错误,你能够读这篇文章 Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error 。(注:这篇文章已翻译)
在生产环境下 Angular 不会抛出错误,可是也不会检查数据变化直到下一次变动检测循环。(注:由于开发者模式下 Angular 会执行两次变动检测循环,第二次检查会发现父组件属性被改变就会抛出错误,而生产环境下只执行一次。)
在 Angular.js 里,每个组件定义了一堆 watchers
来追踪以下数据变化:
在 Angular 里倒是这么实现这些功能的:可使用 OnChanges 生命周期钩子函数来监听父组件属性;可使用 DoCheck 生命周期钩子来监听当前组件属性,由于这个钩子函数会在 Angular 处理当前组件属性变化前去调用,因此能够在这个函数里作任何须要的事情,来获取即将在 UI 中显示的改变值;也可使用 OnInit 钩子函数来监听第三方组件并手动运行变动检测循环。
好比,咱们有一个显示当前时间的组件,时间是由 Time
服务提供,在 Angular.js 中是这么实现的:
function link(scope, element) {
scope.$watch(() => {
return Time.getCurrentTime();
}, (value) => {
$scope.time = value;
})
}
复制代码
而在 Angular 中是这么实现的:
class TimeComponent {
ngDoCheck()
{
this.time = Time.getCurrentTime();
}
}
复制代码
另外一个例子是若是咱们有一个没集成在 Angular 系统内的第三方 slider
组件,但咱们须要显示当前 slide,那就仅仅须要把这个组件封装进 Angular 组件内,监听 slider's changed
事件,并手动触发变动检测循环来同步 UI。Angular.js 里这么写:
function link(scope, element) {
slider.on('changed', (slide) => {
scope.slide = slide;
// detect changes on the current component
$scope.$digest();
// or run change detection for the all app
$rootScope.$digest();
})
}
复制代码
Angular 里也一样原理(注:也一样须要手动触发变动检测循环,this.appRef.tick()
会检测全部组件,而 this.cd.detectChanges()
会检测当前组件及子组件):
class SliderComponent {
ngOnInit() {
slider.on('changed', (slide) => {
this.slide = slide
// detect changes on the current component
// this.cd is an injected ChangeDetector instance
this.cd.detectChanges();
// or run change detection for the all app
// this.appRef is an ApplicationRef instance
this.appRef.tick();
})
}
}
复制代码