[Web翻译]简化Angular变化检测

983 阅读12分钟

副标题:Angular框架核心部分详细介绍

原文地址:medium.com/ngconf/simp…

原文作者:medium.com/@pankajpark…

发布时间:2019年3月1日 - 10分钟阅读

Angular是目前最特别的javascript框架之一,因为它具备了web开发所需的所有功能。变更检测系统是框架的核心,它主要是帮助更新页面上的绑定。在这篇文章中,我们将以简单易懂的方式详细了解变化检测。

在开始之前,我先强调一下现在的框架或库都有哪些原则来构建应用。

一般应用架构

基本上我们有的是我们应用的状态,我们要把它复制到UI上,这就是为什么我们需要在模板上进行数据绑定。之后我们将 "状态+模板 "进行布线,在视图上复制数据。同时在未来,状态发生的任何变化都会反映到视图上。

这个将HTML与状态同步的过程可以称为 "Change detection",每个框架都有自己的方法。React使用虚拟DOM与和解算法,Angular使用变化检测等。本文将介绍变化检测在Angular中的工作原理。

什么是变化检测?

简单来说:-个将模型/状态变化同步到视图的过程。

我们举个简单的例子。假设我们有一个简单的组件,它有自己的HTML,如下图所示。

<span>
  {{title}}
</span>
<button (click)="title='Changed'">
  Change Title
</button>
import { Component } from '@angular/core';
@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  title  = 'Angular'
}

一个简单的绑定例子

app.component.html中有一个简单的span,它显示的是一个组件的title属性,点击后有一个button会修改的title属性值changed

演示

当页面得到发生时,页面上显示 "Angular"。后来当我们点击Change Titlebutton的时候,它就会把绑定值改为 "Changed"。这是超级棒的,Angular框架为我们管理了这个功能。它本质上做的是,跟踪值的变化,并自动反映在UI上。厉害了! 看看上面这个例子的stackblitz

giphy.com/gifs/hulu-s…

魔法

想知道🤔,angular如何更新绑定? 没关系! 其实没有什么事情是真的发生了魔法,一个框架必须在幕后运行一些代码才能做到。这个更新绑定的过程被称为变化检测。但问题是,angular什么时候运行变化检测,如何运行?为了找到这些问题的答案,让我们再进一步挖掘一下。


变更检测应该在什么时候发生?

这个问题的简单答案是 "当应用程序的状态发生变化时"。但是应用程序的状态什么时候可以发生变化呢?🤔

  • 事件回调
  • 网络呼叫(XHR)
  • 计时器(setTimeout, setInterval)

你觉得上面的例子有什么相似之处吗?是的!它们都是异步的。这意味着我们可以简单地说,任何异步调用都可以引起应用状态的变化,这就是我们应该在UI上更新状态的实例。到目前为止,一切都很好!

假设我们要构建自己的变更检测系统,我们会在上述3种情况后启动变更检测。


让我们尝试实现我们自己的变化检测系统。

我们只是确保从每个方法中调用detectChanges方法,其中包括XHR调用、Timer和Event。只是假设 detectChanges 方法负责实际的变更检测。变更检测的模糊实现会像下面的例子一样

//network Call
getDataFromServer().then(()=> {
   //state has changed
   detectChanges();
})

//timeout function
setTimeout(()=> {
   //Call change detection
   detectChanges();
}, 5000)

element.addEventListner('click', (event) =>{
   //state could have change
   detectChanges();
});

detectionChanges方法的实现就像下面的例子一样。

let detectChanges => () => {
  if(currentState != prevState) {
    updateView(currentState);
  }
}

啊!但是在我们现实世界的应用里面做这个事情,会把所有的事情都搞乱。一般在现实世界的应用中,你可能在几十万个地方都有这个东西。那么怎样才能更好的实现这个东西呢?基本上,我们也可以说,我们应该在虚拟机交接的时候,发射变化检测。

ZoneJS来拯救

ZoneJS是一个API,主要是从dart语言移植出来的。在幕后,ZoneJS猴子打补丁的功能。基本上,它有助于关注所有的异步任务,并提供在任务完成之前或之后调用代码的能力。Zone API有不同的钩子来放置你的代码onBeforeTaskonAfterTaskonZoneCreatedonError

var myZoneSpec = {
  beforeTask: function () {
    console.log('Before task');
  },
  afterTask: function () {
    console.log('After task');
  }
};

var myZone = zone.fork(myZoneSpec);
myZone.run(function() {
   console.log('My task successfully got executed.')
});

// Output:
// Before task
// My task successfully got executed.
// After task

ZoneJS的简单例子

所以,Angular利用Zones的力量来启动变化检测。事情是这样的,如果发生任何异步调用,ZoneJS API会向onMicrotaskEmpty observable发出数据,Angular会根据同样的数据调用 detectChanges方法。


当应用程序启动时,会发生什么?

当一个angular应用引导时,它为一个引导的angular模块创建了一个平台。它为整个模块创建了一个ApplicationRef。基本上,ApplicationRef有对allcomponentscomponentTypesisStable(区域标志)的引用,也有detatchViewattachViewtick等方法。你可以看看源代码中的这一行

让我们快速查看一下Angular源码中的application_ref.ts。你会看到,在创建一个ApplicationRef后,它在onMicrotaskEmpty观测值上放置了一个订阅,所以一旦VM tick结束,它就会向onMicrotaskEmpty观测值发出一个值,这个值会被一个订阅所监听,最终会在当前Zone的应用程序中调用tick方法。

this._zone.onMicrotaskEmpty.subscribe({
  next: () => { 
    this._zone.run(() => { 
      this.tick();
    }); 
  }}
);

我们来看看tick方法是如何工作的。

  tick(): void {
    try {
      this._views.forEach((view) => view.detectChanges());
      if (this._enforceNoNewChanges) {
        this._views.forEach((view) => view.checkNoChanges());
      }
    } catch (e) {
      ...
    } finally {
      ...
    }
  }

tick方法的实现看起来很简单。它基本上是循环处理每个视图(组件内部称为view),然后调用它们的detectChanges方法,该方法负责更新UI绑定。有趣的是在第四行,它只在开发模式下运行,因为在application_ref.ts构造函数中,它设置了_enforceNoNewChanges属性。

this._enforceNoNewChanges = isDevMode()

以上是关于变更检测的工作原理的要点,让我们深入了解一下,在制作Angular应用的时候,我们如何使用它。


变更检测策略

总的来说,angular有两种口味的变化检测。

  1. 默认的
  2. OnPush

让我们来看看每个变化检测策略。

Default

当你没有在Component decorator上指定变化检测策略时,默认情况下,angular会应用Default策略。

任何Angular应用都是由组件组成的,组件在哪里,我们就在哪里bootstrap一个根组件.而我们可以就一个组件画出一个应用的图。所以,如果在任何组件中发射变化检测,就会导致ApplicationRef中的tick方法被发射。最终从根组件到它的子孙组件发射 detectChanges 方法,如下图所示。

变化检测策略 - Default

缺省策略的问题是,在任何一个组件上检测到的变化都会导致对所有组件进行变化检测(如果所有组件都设置为Default策略的话),但大多数情况下我们不需要这样的行为,它最终会通过运行多个不必要的变化检测周期来影响应用程序的性能。但大多数时候我们并不需要这样的行为,它最终会因为运行多个不必要的变化检测周期而影响应用程序的性能。

我们能否有效地解决这个问题呢?幸运的是有一个解决方案,你可以很容易地将变更检测策略切换到OnPush

OnPush

onPush策略使组件的变化检测更加智能。它只在组件的输入绑定值发生变化时才运行组件的变化检测。实际上,它比较的是一个Input绑定的oldValue和newValue之间的引用。这意味着如果一个对象的父组件的属性没有变化,它不会触发该组件的变化检测。

变化检测策略 - OnPush

如上图所示,我们设置了一个1级组件为OnPush策略。两个组件的输入都是name 。根组件将输入的name分别传递给两个组件,分别为name1name2。在初始页面加载时,所有组件的变化检测都会启动。而后右侧组件发出一个事件,趋向于改变根组件的状态。

所以,同样变化检测从根组件开始发射。然后,变化检测为第1级(OnPush)组件运行。在对这些组件启动变化检测之前,它会检查输入绑定name的newValue和oldValue,如果有变化,那么只对该组件及其子代启动变化检测。右手边的组件已经被检测到变化。因此,变化检测只对右边的组件分支进行触发。通过设置OnPush变化检测策略,我们可以显著提高应用程序的性能。

当使用组件OnPush策略时,请确保你在Input绑定值上执行不变性。


在继续之前,我们可以看看一个使用两种变化检测策略构建的真实应用程序。这个应用程序部署在pankajparkar.github.io/demystifyin…

这是一个非常简单的应用,它的页面上显示有帖子,每个帖子可以有评论。该应用程序的架构方式,它的组件层次结构如下所示。

root => post-list (所有帖子)=> post (单个帖子)=> comment-list。

组件周围的黑色边框表示该特定组件的边界。

由于我们要时刻关注变化检测器什么时候发火,所以一旦变化检测器发火,我们就将组件高亮显示为黄色。我们使用了ngAfterViewChecked,它告诉我们变化检测器已经访问了当前组件。

ngAfterViewChecked(): void {
  this.zone.runOutsideAngular(() => {
    this.el.nativeElement.classList.add('highlight')
    setTimeout(() => {
      this.el.nativeElement.classList.remove('highlight')
    }, 1500)
  })
}

在检测到变化时高亮组件

所有组件默认策略

所以你可以看到在上图中,在初始页面加载时被高亮,并应用highlight类。此后,当在注释字段中添加注释时,你可以看到,在每个keyup事件中,它都会启动变化检测,所有的组件都会被高亮显示。


现在,看看OnPush变更检测策略的用途,它在变更检测运行周期中是如何发挥作用的。

现在所有的组件都设置为OnPush策略。所以在页面加载时,所有组件的变化检测都会运行,这完全没有问题。此后,当我试图在评论部分添加文本时,它为当前组件的评论部分启动变化检测,而不是其他组件。这真是太好了! 但你可以看到,其他组件后组件被高亮显示。

啊!这是怎么回事?PostComponent已经被设置为OnPush,该组件上没有Input绑定,但它似乎被改变了。这是不是一个bug,是不是ngAfterViewChecked的生命周期钩子被无故调用了?也许是吧。

我们不要搞混了。我们可以进一步研究一下。

摘自Max Koretskyi在@angularInDepth的文章

请参考上图--当变化检测在为父组件运行时,它遵循一定的流程。最初,它更新子组件的绑定,然后调用子组件的ngOnInitngDoCheckngOnChanges生命周期钩子。我们也可以说明,这个过程发生在渲染父组件之前。然后它更新当前组件的DOM。之后,它运行子组件的变化检测(取决于策略),然后调用钩子ngAfterViewChecked , ngAfterViewInit

也就是说,当运行父组件的变化检测时,无论组件的变化检测策略如何,它都会运行子组件的ngDoCheck, ngContentChecked, ngAfterViewChecked生命周期钩子。

查看Pascal Prechtʕ-̫͡-ʔ记录的这个Github问题链接

你可能忽略了,有一个问题。如果你看一下PostListComponent的变化检测策略,它已经被设置为OnPush策略,但是没有输入绑定传递给它。因此,当PostListComponent组件从ngOnInit钩子中获取数据时,它不会从根组件(AppComponent)中运行变化检测。但是它阻止在PostListComponent上运行变化检测,因为没有Input被改变。所以我们必须调用 ChangeDetectorRef 依赖的 detectChanges 方法或 markForCheck 方法。这样就会强制变化检测全程运行。在实际应用中,这样的情况很容易发生。你可以通过调用markForCheckdetectChanges来解决这种情况。


TL;DR

调用markForCheckdetectChangestick之间的区别是

markForCheck - 一旦你在组件变化检测器上调用markForCheck方法,它将遍历组件树直到根,并标记这些组件,只在下一次迭代时运行变化检测。即使被标记的组件使用OnPush策略,它也会在这些组件上运行变化检测。

detectChanges - 当你在 changeDetectorRef 提供者上调用这个方法时,它将从当前组件和它的所有后裔中运行变化检测。当运行变化检测时,它将保持变化检测策略在头脑中。

tick -适用于ApplicationRef API的tick方法。它将从根组件到它的所有后裔运行变化检测。它尊重组件的变化检测策略。

还有两个方法存在于ChangeDetectorRef提供者中。

detach - 通过调用这个方法,你可以从当前组件的树上拔出一个组件到它的子孙。每当需要在组件上运行变化检测时,你可以根据你的需要调用 detectChangesmarkForCheck 方法。

reattach --通过调用reattach方法,可以很容易地将一个从树上摘下来的组件恢复到原来的位置。这可以用于微调应用程序的性能。


希望这篇文章能帮助你了解变化检测的奥秘。最终,这也将使您能够自如地预测应用程序中变更检测的运行时间。通过应用各种风味,你可以轻松地在你的应用程序中获得性能优势。

最近,我在#ngIndia中就这个话题做了一次演讲。

www.youtube.com/watch?v=XTN…

如果您喜欢这篇文章请按👏 拍手键50次或任意次数。如果有什么问题,请随时提问。非常感谢你的阅读

感谢Tayamba Mwanza的语法审查😊。


通过( www.DeepL.com/Translator )(免费版)翻译