【问题标题】:How does Angular 2 change detection work?Angular 2 变更检测如何工作?
【发布时间】:2016-05-29 21:13:50
【问题描述】:

在 Angular 1 中,变更检测是通过对 $scope 层次结构的脏检查来实现的。我们会在模板、控制器或组件中隐式或显式地创建观察者。

在 Angular 2 中,我们不再有 $scope,但我们确实覆盖了 setInterval、setTimeout 等。我可以看到 Angular 如何使用它来触发 $digest,但 Angular 如何确定发生了什么变化,尤其是考虑到 Object.observe 从未进入浏览器?

示例

这是一个简单的例子。服务中定义的对象在 setInterval 中更新。每个间隔都会重新编译 DOM。

Angular 如何判断 AppComponent 正在监视服务,以及服务的属性值发生了变化?

var InjectedService = function() {
  var val = {a:1}
  setInterval(() => val.a++, 1000);
  return val;
}

var AppComponent = ng.core
  .Component({
    selector: "app",
    template:
    `
      {{service.a}}
    `
  })
  .Class({
    constructor: function(service) {
      this.service = service;
    }
  })

AppComponent.parameters = [ new ng.core.Inject( InjectedService ) ];

document.addEventListener('DOMContentLoaded', function() {
  ng.platform.browser.bootstrap(AppComponent, [InjectedService])
});

【问题讨论】:

标签: angularjs angular


【解决方案1】:

Angular 为每个组件创建一个更改检测器对象(参见ChangeDetectorRef),它跟踪每个模板绑定的最后一个值,例如{{service.a}}。默认情况下,在每个异步浏览器事件(例如来自服务器的响应、单击事件或超时事件)之后,Angular 更改检测都会执行,并使用这些更改检测器对象对每个绑定进行脏检查。

如果检测到更改,则传播更改。例如,

  • 如果输入属性值发生更改,新值将传播到组件的输入属性。
  • 如果 {{}} 绑定值发生更改,新值将传播到 DOM 属性 textContent。
  • 如果x 的值在样式、属性或类绑定中发生变化——即[style.x] 或[attr.x] 或[class.x]——新值将传播到DOM 以更新样式、HTML 属性,或类。

Angular 使用 Zone.js 创建自己的区域 (NgZone),它会为所有异步事件(浏览器 DOM 事件、超时、AJAX/XHR)打补丁。这就是变更检测能够在每个异步事件之后自动运行的方式。即,在每个异步事件处理程序(函数)完成后,将执行 Angular 更改检测。

我在这个答案中有更多详细信息和参考链接:What is the Angular2 equivalent to an AngularJS $watch?

【讨论】:

  • 那么我是否应该说每次发生任何变化时它仍在检查所有内容?
  • @superluminary,是的,默认情况下,每次更改检测运行时它都会检查所有模板绑定。 (如果一个组件被标记为OnPush,如果输入属性没有改变,它可能会跳过该组件。)
【解决方案2】:

Zone.js

更改是作为对某事的反应而发生的,因此在这方面它们是异步的。它们是由异步动作引起的,在浏览器世界中它们是Events。为了拦截这些事件,Angular 使用了 zone.js,它会修补 JavaScript 调用堆栈(我相信,如果我错了,有人会纠正我)并公开可用于执行其他操作的钩子。

function angular() {...}
zone.run(angular);

如果你想象这个angular 函数是整个 Angular,那么这就是它在区域中运行的方式。这样做可以拦截事件,如果它们被触发,我们可以假设发生了变化,并监听/观察它们。

应用程序参考

实际上ApplicationRef 创建了区域:

/**
 * Create an Angular zone.
 */
export function createNgZone(): NgZone {
  return new NgZone({enableLongStackTrace: assertionsEnabled()});
}

而NgZone 类是用few event emitters 创建的:

  this._onTurnStartEvents = new EventEmitter(false);
  this._onTurnDoneEvents = new EventEmitter(false);
  this._onEventDoneEvents = new EventEmitter(false);
  this._onErrorEvents = new EventEmitter(false);

它通过 getter 暴露给外界:

  get onTurnStart(): /* Subject */ any { return this._onTurnStartEvents; }
  get onTurnDone() { return this._onTurnDoneEvents; }
  get onEventDone() { return this._onEventDoneEvents; }
  get onError() { return this._onErrorEvents; }

当ApplicationRef 是created 时,它订阅区域的事件,特别是onTurnDone():

this.zone.onTurnDone
  .subscribe(() => this.zone.run(() => this.tick());

变化

当事件被触发时,tick() 函数运行,循环遍历每个组件:

  this._changeDetectorRefs.forEach((detector) => detector.detectChanges());

和detects changes 基于组件的ChangeDetectionStrategy。这些更改被收集为SimpleChange 对象的数组:

addChange(changes: {[key: string]: any}, oldValue: any, newValue: any): {[key: string]: any} {
  if (isBlank(changes)) {
    changes = {};
  }
  changes[this._currentBinding().name] = ChangeDetectionUtil.simpleChange(oldValue, newValue);
  return changes;
}

witch 可以通过onChanges 接口为我们提供:

export interface OnChanges { 
  ngOnChanges(changes: {[key: string]: SimpleChange}); 
}

【讨论】:

  • "loops through each component" -- 虽然你提到了ChangeDetectionStrategy,但我认为值得注意的是,如果一个组件被标记为OnPush,它通常会被跳过,除非它特别是marked/flagged for change detection .如果已标记,则检查它,然后取消标记。
  • "那些更改被收集" -- 我认为值得注意的是,只有输入属性更改被收集并通过ngOnChanges() 传递给我们。不包括其他更改(例如,{{}} 等模板绑定)。并且由于通过比较值来检测变化,如果输入属性是 JavaScript 引用类型(例如,数组、对象等),我们只有在引用(值)发生变化时才会收到通知。如果数组 ref 没有改变,但是,比如说,数组的一个项目发生了改变,ngOnChanges() 不会通知我们。 (我提到这一点是因为这种情况在 SO 上出现过几次。)
猜你喜欢
  • 2017-09-28
  • 2017-05-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-15
  • 1970-01-01
  • 2017-05-04
相关资源
最近更新 更多