OnPush策略

160 阅读8分钟

什么情况下适合使用onpush?,参考: zhuanlan.zhihu.com/p/56176553

1. Change Detection(变更检测)是什么?

前端展示的页面是由视图和数据共同构成的,视图模板定义了页面的框架,而数据定义了页面具体的显示内容。而数据发生变化的时候,我们需要及时将变化的内容更新到视图中,否则用户看到的数据就是不正确的。系统及时感知到数据模型的变化,然后通过计算更新到视图中,这是每个前端框架都需要解决的问题。这就是所谓的变化检测。

如您所知,angular应用程序是一颗自顶向下的组件树,对应的,每个组件都拥有自己独立的变化检测器,这些变化检测器也组成了一颗自顶向下的和组件树一 一对应的树。顺便说一句,这些变化检测器是由Angular编译器为每个组件自动创建的、JavaScript VM友好的代码,这就意味着变化检测是快速的 (相比于 Angular 1.x 的 $digest)。基本上,每个组件可以在几毫秒内执行数万次检测。Angular的数据流是自顶而下,从父组件到子组件单向流动。单向数据流向保证了高效、可预测的变化检测。尽管检查了父组件之后,子组件可能会改变父组件的数据使得父组件需要再次被检查,但这是不被推荐的数据处理方式。在开发模式下,Angular会进行二次检查,如果出现上述情况,二次检查就会报错:ExpressionChangedAfterItHasBeenCheckedError。而在生产环境中,脏检查只会执行一次。

2. 变更检测的策略

2.1 值类型vs.引用类型

为了理解变更检测的策略以及它是如何工作的,我们必须首先理解JavaScript中值类型与引用类型的区别。

值类型只是将它们的值存储在堆栈内存上 (从技术上讲, 这不完全是正确的, 但这对本文来说已经足够了)。

而引用类型在堆栈内存上存储一个引用, 该引用指向它们在堆内存上的实际值。对于Angular来说,改变引用类型的某一属性值,angular认为这个变量是没有变的,因为编译器看到的引用地址没变。

值类型和引用类型的重要区别在于, 为了读取值类型的值, 我们只需查询堆栈内存, 但为了读取引用类型的值, 我们需要首先查询堆栈内存以获取引用然后使用该引用查询堆内存以查找引用类型的值。

2.2 数据何时变化

接下来的问题是,数据何时变化,哪些因素会引起数据变化?其实很好理解,所有的异步操作是可能导致数据变化的根源因素,因而,当发生异步操作时,变更检测就应该被触发。这些异步操作可大致归为以下几类:

  • 用户输入操作及事件,比如click, keyup,submit等
  • 请求服务端数据,如xmlHttpRequest
  • 定时事件,比如setTimeout,setInterval

2.3 如何通知变化

那么,在Angular中是谁来通知数据即将变化的呢?在AngularJS中是由代码scope.scope.apply()或者scope.scope.digest触发,而Angular接入了ZoneJS,由它监听了Angular所有的异步事件。ZoneJS是怎么做到的呢?其实它重写了所有的异步api(所谓的猴子补丁Monkey patch)!ZoneJS会通知Angular可能有数据发生变化,需要检测更新.

2.4 angular 中提供的两种变更检测策略

在Angular中,变更检测策略有两种,OnPush和Default

为了更直观的解释这两种策略的区别,我们建立下面这个假设,父子组件通过vData这个Input进行传值,

对于default策略,假设vData是值类型,这意味着父组件改变了vData的值,那么势必会触发变更检测来使子组件的Input得以同步。而对于引用类型,即使父组件只是改变了引用类型中的值,也同样会变更检测。当然,对于不可变对象,这更不必多言。也就是说,可以理解default策略是只要vData有任何的变化,变更检测都会触发。为什么? 您可能会问。其实很好理解:"Angular has to be conservative and run all the checks every single time because the JavaScript language does not give us object immutation guarantees."

而对于OnPush策略,假设vaData是值类型,父组件改变了vData的值,那么会触发变更检测。而对于引用类型,如果父组件只是改变了引用类型中的值,对于子组件来说,vData其实没有变,因为它所看到的vData是引用,是指针,引用没变,那么变更检测就不会触发,那对应很好理解的就是,如果vaData的引用变了,即vData被重新赋值指向了别的对象,此时才会触发变更检测。当然,对于不可变对象,父组件即使只是改变了引用类型的值,对于编译器而言,这个对象也已经不是原来的对象了,变更检测还是会触发。

两者的区别可以理解了吧?,如果不是太理解,请您看一下JavaScript中可变对象与不可变对象的区别,您就明白了。简单来说,对于Angular中的不可变对象,即使只是改变了其某一属性的值,Angular也认为这个对象已经不是原对象了。

重要的事情说第一遍,即使是子组件设置了Onpush策略,对于发生在该子组件中的异步事件,同样会引发变更检测,切勿理解错误。事实上,对于设置了onpush的子组件,只有当该子组件内发生了异步事件或者它的input传值发生了变化(值类型的值变化或者引用类型的引用变化或者不可变类型变化),才会触发该子组件的变更检测。

3. OnPush策略如何影响变更检测的过程

在经过上述的铺垫之后,接下来,我们重点的介绍在angular中,OnPush策略如何影响变更检测的过程。在此之前,我们通过两幅图来回顾一下angular的变化检测过程。

\

\

如你所见,假设对于父子组件间输入传值频繁变化的情况,或者这颗组件树变得越来越庞大的时候,如果我们只采用angular默认的变更检测default策略,对于应用中发生的任一异步事件或者父子组件间input变化(值类型的值变化或者引用类型的引用变化或者不可变类型变化),都将带来越来越高额的变更检测开销,直至这一开销到最后严重影响到我们应用的性能。想办法缩减这一开销,有益无害。怎么缩减,这就涉及到了上述的OnPush策略,也即是我们去手工干预父子组件传值情况下的变更检测过程,实现我们的自定义。在父子组件传值的这个input变化不那么频繁的情况下,更多的使用onpush策略,而即使这个input变化很频繁,我们也可以合理的手工干预,尽可能的对组件树进行修剪,这就是在变更检测中的性能优化。

下面为了更直观的理解设置了OnPush之后如何影响到了变更检测的过程,会通过示例图来辅助说明。在开始之前,假设我们把所有父子组件间传值策略都设置为OnPush,组件树就变成了这样

\

\

我们把目光聚焦在D子组件及其子组件组成的这条链路上(图中的绿色框),现在分别考虑以下几种情况下变更检测将会涉及到的链路。(AD组件间input为Data)

  • Data为值类型,A组件改变了Data的值

\

\

OnPush策略对值类型保持变更检测,没毛病

  • Data为引用类型,A组件改变了Data的属性值

\

\

Data的引用实际上没有变化,OnPush策略下不触发变更检测

  • Data为引用类型,A组件改变了Data的引用指向,即给Data重新赋值新对象

\

\

Data的引用发生了变化,OnPush策略下触发变更检测

  • Data为不可变对象,A组件对Data的任何改变

\

\

由于Data为不可变对象,此时angular认为Data都不是原来的值,onpsh策略下会触发变更检测

  • A组件中发生了异步事件

\

\

由于Data没有变化,这条链路不会发生变更检测

  • D组件中发生了异步事件

\

\

即使Data没有变化,但发生在B组件的异步事件依旧会触发变更检测

  • D组件改变了其子组件E的input传值

\

\

这里需要格外注意,Data虽没有变化,但是D想要改变E的input的值,必然在D中会发生异步事件,所以D组件中会触发变更检测,但是E因为是OnPush,所以如果D改变的input只是引用类型中属性的变化,E中是不会触发变更检测的,否则,E也会触发变更检测。

  1. D组件的子组件E中发生了异步事件

\

\

即使Data没有变化,但这条链路依旧会触发变更检测

  • 这条目标链路之外的任一组件发生了异步事件

\

\

Data没有变化,所以这条链路不会触发变更检测

重要的事情说第二遍,关于异步事件,综合以上案例,我们会发现一个现象就是,任一组件发生了异步事件,一定会至少触发从根到该组件这条链路上的所有组件的变更检测。这里说至少,意味着如果该组件下面还有子组件,子组件发不发生变更检测分两种情况:1 该组件下的子组件设置了default策略,那么该子组件下的子组件还会变更检测。2 该组件下的子组件设置了OnPush,那么只有当下属子组件的input变化(值类型的值变化或者引用类型的引用变化或者不可变类型变化)才会触发变更检测。

综合以上案例,相信您对OnPush与变更检测之间的联系有了一个清晰的认识,甚至对如何合理使用OnPush进行性能优化心里有些谱了。事实上对于所有的组件都采用OnPush肯定不是一个最合理的解决之道,我们要做的就是可以对父子组件Input变化不那么频繁的情况下,设置子组件为OnPush策略,便可以优化掉该子组件乃至它的子组件所组成的一整条链路发生变更检测的频率.