共计 2312 个字符,预计需要花费 6 分钟才能阅读完成。
内容来自于 Max Koretskyi aka Wizard 的《A gentle introduction into change detection in Angular》
初次相遇
让我们从一个简单的 Angular 组件开始。他表现应用程序的变化检测。这个时间戳的精度为毫秒。点击 triggers 按钮触发检测:
@Component({
selector: ‘my-app’,
template: `
<h3>
Change detection is triggered at:
<span [textContent]=”time | date:’hh:mm:ss:SSS'”></span>
</h3>
<button (click)=”0″>Trigger Change Detection</button>
`
})
export class AppComponent {
get time() {
return Date.now();
}
}
如你所见,这是相当基本的。有一个名为 time 的 getter 返回当前时间戳。并且,我将它绑定到 HTML 中的 span 元素。当 Angular 运行变化检测时,它获取 time 属性的值,通过日期管道传递它,并使用结果更新 DOM。这一切都很正常,但当我打开控制台的时候,我看到了一个错误:ExpressionChangedAfterItHasBeenCheckedError。
事实上,这让我们感到非常惊讶。通常这个错误出现在更加复杂的程序上。但为什么一个如此简单的功能会导致这个错误呢?别担心,我们现在就来查看他的原因。让我们先从错误消息开始:
Expression has changed after it was checked. Previous value:“textContent: 1542375826274”. Current value:“textContent: 1542375826275”.
它告诉我们,textContent 绑定的值是不同的。的确,毫秒不相同。因为 Angular 通过表达式 time | date:’hh:mm:ss:SSS’ 计算了两次,并比较了结果。它检测到了两次值的差异,这就是导致错误的原因。
但 Angular 为什么要这样做?或者它什么时候做的?在我们了解这些问题的答案之前,我们还需要了解另外一些东西。组件视图和绑定 Angular 的变化检测主要有两个部分:
组件视图
相关绑定
每一个 Angular 的组件都有一个 HTML 元素。当 Angular 创建 DOM 节点并将内容渲染到屏幕上,它需要一个地方来储存 DOM 节点的引用。为了实现这一目标,Angular 内部有一个被称为 View 的数据结构。它还用于存储对组件实例的引用和绑定表达式之前的值。并且视图和组件之间的关系是一一对应的。下图展示了该关系:
当编译器分析模板时,它会辨识在变化检测期间可能需要更新的 DOM 元素属性。每一个这样的属性,编译器都会创建一个绑定。绑定定义要更新的属性名和 Angular 用来获取新值的表达式。
在我们的例子当中,time 属性用于 textContent 的表达式中。所以,Angular 会创建绑定来连接它和 span 元素。
实际上,绑定不是包含所有必要信息的单个对象。viewDefinition 定义模板元素和要更新的属性的实际绑定。用于绑定的表达式在 updateRenderer 方法中。
* 检查组件视图如你所知,Angular 会对每一个组件执行变化检测。现在我们知道每个组件在 Angular 内部被称为视图(view),我们可以说 Angular 对每个视图执行了变化检测。
当 Angular 检查视图时,它只需运行编译器为视图生成的所有绑定。它计算表达式并将它们的结果与视图上旧值数组中存储的值(oldValues)进行比较。这就是脏检查这个名字的由来。如果检测到差异,它会更新与绑定相关的 DOM 属性。它还需要将新值放入视图的旧值数组中。就这样。您现在有了更新的用户界面。一旦完成当前组件的检查,它将对子组件重复完全相同的步骤。在我们的应用程序中,在 App 组件中 span 元素的属性 textContent 只有一个绑定。所以在变化检测期间,Angular 会读取组件 time 属性的值,再使用 date 管道,并将它与视图中存储的先前值进行比较。如果检测到不同,Angular 会更新 span 旧值(oldValues)数组中的 textContent 属性.
但是错误又从哪里出来的呢?在开发模式下,每个变化检测周期之后,Angular 会同步运行另外一个检查,已确保表达式产生的值与之前变化检测运行期间的值相同。该检查不是原始检查的一部分,它在对整个组件树的检查完成后运行,并执行完全相同的步骤。然而,当这一次变化检测期间,如果检测到不同那个的值,Angular 不会去更新 DOM,相反的,它会直接抛出错误 ExpressionChangedAfterItHasBeenCheckedError。
但是 Angular 为什么要这样做?现在我们知道什么时候抛出错误了。但是为什么 Angular 需要这个检测。假设在变化检测运行期间,又有一些组件的一些属性被更新。此时,表达式产生的新值与用户界面中呈现的值不一样。这个时候 Angular 应该怎么做?它当然也可以另外再运行一个变化检测周期来使应用程序状态与用户界面同步。但如果在这期间,又有一些属性被更新了呢?看到问题了吗?实际上 Angular 可能会在变化检测的无限循环中结束。这种情况在 AngularJS 中经常发生。为了避免这种事情,Angular 强制让数据单向流动。这种在变更检测和结果表达式变更后运行的检查是强制机制。一旦 Angular 处理了当前组件的绑定,就不能再更新绑定表达式中使用的组件属性。