从一个小Demo看React的diff算法

前言

React的虚拟Dom和其diff算法,是React渲染效率远远高于传统dom操做渲染效率的主要缘由。一方面,虚拟Dom的存在,使得在操做Dom时,再也不直接操做页面Dom,而是对虚拟Dom进行相关操做运算。再经过运算结果,结合diff算法,得出变动过的部分Dom,进行局部更新。另外一方面,当存在十分频繁的操做时,会进行操做的合并。直接在运算出最终状态以后才进行Dom的更新。从而大大提升Dom的渲染效率。
对于React如何经过diff算法来对比出作出变更的Dom,React内部有着复杂的运算过程,此文不作具体代码层级的讨论。仅仅经过一个小小Demo来宏观上的探讨下diff的运算思路。javascript

diff的对比思路

React的diff对比是采用深度遍历的规则进行遍历比对的。如下图的Dom结构为例:
avatar
对比过程为:对比组件1(没有变化)-> 对比组件2(没有变化)-> 对比组件4(没有变化)-> 对比组件5(组件5被移除,记录一个移除操做)-> 对比组件3(没有变化)->对比组件3子组件(新增了一个组件5,记录一个新增操做)。对比结束,此时变更数据记录了两个节点的变更,在渲染时,便会执行一次组件5的移除,和一次组件5的新增。其它节点不作变动,从而实现页面Dom的更新操做。html

Demo初探

接下来,咱们设计一个简单的demo,来分析页面变化时的整个过程。
首先咱们建立几个相同的Demo组件:java

import React, { Component } from 'react';
    export default class Demo1 extends Component {
        componentWillMount() {
            console.log('加载组件1');
        }
        componentWillUnmount() {
            console.log('销毁组件1')
        }
        render () {
            return <div>{this.props.children}</div>
        }
    }

组件除了将其内部的Dom直接渲染以外,还在组件加载前和卸载前分别在控制台中打印出日志。
接下来经过代码组合出上图中的组件结构,并经过事件触发组件结构的变化。react

// 变化前
    <Demo1>1
        <Demo2>2
            <Demo4>4</Demo4>
            <Demo5>5</Demo5>
        </Demo2>
        <Demo3>3</Demo3>
    </Demo1>
    
    // 变化后
    <Demo1>1
        <Demo2>2
            <Demo4>4</Demo4>
        </Demo2>
        <Demo3>3
            <Demo5>5</Demo5>
        </Demo3>
    </Demo1>

执行变动操做以后,控制台会打印出日志算法

加载组件5
    销毁组件5

结果通分析中同样,分别执行了一次组件5的加载操做和一次组件5的卸载操做。
接下来来分析一些复杂的状况。
首先看下面这种Dom的删除
avatar
按照前面的分析,比对过程为:对比组件1(没有变化)-> 对比组件2(没有变化)-> 对比组件4(组件4被移除,记录一个移除操做)-> 对比组件5(没有变化)-> 对比组件6(没有变化)-> 对比组件3(没有变化)。对比结束。按照这个分析,用代码进行测试后,控制台日志应该会输出:数组

销毁组件4

这一条日志。然而,在实际测试后,会发现输出日志为:dom

加载组件5
    加载组件6
    销毁组件4
    销毁组件5
    销毁组件6

能够发现,除了“销毁组件4”这一个操做以外,还进行了组件5和组件6的销毁和加载操做。难道是咱们以前的分析是错误的?别急,咱们再来进行另一个实验:
avatar
一样只删除了一个组件,只是删除的组件位置不一样,按照上次的实验结果,控制台输出日志应该为:测试

加载组件4
    加载组件5
    销毁组件4
    销毁组件5
    销毁组件6

然而,实际的实验结果又出乎咱们的预料。实际输出结果仅为:this

销毁组件6

这个现象十分有趣。仅仅是删除了不一样位置的组件,diff分析的过程却彻底不同。其实,若是你继续实验删除组件5,你会发现,所得的结果跟前两次也是彻底不一样。
其实diff算法在进行虚拟Dom的变动比对时,并不能精确的进行一对一的比对(固然react提供了解决方案,后面讨论)。当一个父节点发生变动时,会销毁掉其下全部的子节点。而其兄弟节点,则会按照节点顺序进行一对一的顺序比对。那么在上面第一个例子的比对顺序实际上是这样的:对比组件1(没有变化)-> 对比组件2(没有变化)-> 对比组件4(组件4变动为组件5,记录一次组件4的移除操做和一次组件5的新增操做)->对比组件5(组件5变动为组件6,记录一次组件5的移除操做和一次组件6的新增操做)->对比组件6(组件6被移除,记录一次组件6的移除操做)。对比结束。按照这个分析思路,控制台的输出结果就不难理解了。
一样当咱们在第二个例子中移除组件6时。组件4和组件5的顺序并无变化,因此对比时,仍然是跟自身组件的虚拟Dom进行比对,没有变化,因此也就只有一次组件6的移除操做。
咱们能够进一步经过新增及修改操做来进一步验证猜测。
经过在组件4前新增一个组件和在组件6后新增一个组件的对比。能够发现结果与咱们的猜测结果彻底一致。具体实验推演过程,此处不在赘述。
对于修改,因为修改并未改变该组件及其兄弟组件的个数及顺序,因此仅仅会执行替换组件及其子组件的新增操做和被替换组件的移除操做。
同级的组件分析完了,那么若是是跨层级的组件操做呢?好比下面这种dom变动:
avatar
这种变动,因为组件2,组件4,组件5三个组件的结构均未有任何变化,那么会不会复用其整个结构,只进行相对位置的变动呢?实验发现,控制台日志输出为:spa

加载组件3
    加载组件2
    加载组件4
    加载组件5
    销毁组件2
    销毁组件4
    销毁组件5
    销毁组件3

可见组件2及其子组件发生变化时,组件2以及其下的全部子组件均会被从新渲染。那么为何组件3也会从新渲染呢?其实缘由并非其增长了子节点,而是由于其兄弟节点2被移除,影响了其相对位置而形成的。其完整的对比流程为:对比组件1(没有变化)-> 对比组件2(组件二变动为组件3,记录一次组件2的移除操做以及其子组件:组件4和组件5的移除操做,同时记录组件3的新增操做,以及其子组件:组件2,组件4和组件5的移除操做)-> 对比组件3(组件3被移除,记录一次组件3的移除操做
分析可见:当一个节点变化时,其下的全部子节点会所有被从新渲染。好比在上个例子中,不进行结构的变动,只是将组件2替换为组件6,组件4和组件5保持不变,但因为组件4和组件5是组件2的子组件,组件2的变动依然会致使组件4和组件4被从新渲染。
此外,分析输出的结果,能够看到,react在进行局部Dom的更新时,会先执行新组件的加载,再执行组件的移除操做。

被忽略的key

在咱们之前的开发工做中,确定遇到过列表的渲染。此时React会强制咱们为列表的每一条数据设置一个惟一的key值(不然控制台会报警告),而且官方禁止使用列表数据的下标来做为key值。在React 16及之后版本中,新增的以数组的形式来渲染多个同级的兄弟节点的写法中,一样要求咱们为每一项添加惟一key值。你可能很疑惑这个必须加的key,彷佛并无什么实质的做用,为什么倒是一个必加项。

渲染效率的提高

其实,在React进行diff运算时,key值是十分关键的,由于每个key就是该虚拟Dom节点的身份证,在咱们以前的实验中,因为没有定义key值,diff运算在进行虚拟Dom的比对时,并不知道这个虚拟Dom跟以前的哪一个虚拟Dom是同样的,因此只能采用顺序比对的方案,进行一对一比对。因此才有了以前分析中的因为位置的不一样,致使了彻底不一样的输出结果。而当咱们为每个组件添加key值以后,因为有了惟一标示,在进行diff运算时,便能进行精确的比对,再也不受到位置变更的影响。
回到最初的删除实验,为每个组件添加上惟一的key:
avatar

// 变化前
    <Demo1 key={1}>1
        <Demo2 key={2}>2
            <Demo4 key={4}>4</Demo4>
            <Demo5 key={5}>5</Demo5>
            <Demo6 key={6}>6</Demo6>
        </Demo2>
        <Demo3 key={3}>3</Demo3>
    </Demo1>
    
    // 变化后
    <Demo1 key={1}>1
        <Demo2 key={2}>2
            <Demo4 key={4}>4</Demo4>
            <Demo5 key={5}>5</Demo5>
            <Demo6 key={6}>6</Demo6>
        </Demo2>
        <Demo3 key={3}>3</Demo3>
    </Demo1>

运行发现,其输出日志正是咱们最初设想的那样:

销毁组件4

相对于没有key值的操做,避免了组件5和组件6的从新渲染。大大提升了渲染的效率。此时,为何列表类数据必须加一个惟一的key值,就显而易见了。试想一下在一个无限滚动的移动端列表页面,加载了1000条数据。此时将第一条删除,那么,在没有key值的状况下,要从新渲染这个列表,须要将第一条以后的999条数据所有从新渲染。而有了key值,仅仅只须要对第一条数据进行一次移除操做就能够完成。可见,key值对渲染效率的提高,绝对是巨大的。

key不可设置为数据下标

那么,为何不能将key值设置为数据的下标呢?其实很简单,由于下标都是从0开始的,仍是这个移动端的列表,删除了第一条数据,若是将key值设置为了数据下标。那么原来的key值为1的数据,在从新渲染后,key值会从新被设置为0,那么在进行比对时,会把这条数据跟变动前的key为0的数据进行比对,很明显,这两条数据并非同一条,因此依然会由于数据不一样,而致使整个列表的从新渲染。

key值必须惟一?

除此以外,还有一个开发中的共识,就是key值必须惟一。但key值真的不能相同吗?
按照以前的实验以及分析,能够看出:当在进行兄弟节点的比对时,key值可以做为惟一的标示进行精确的比对。可是对于非兄弟组件,因为diff运算采用的是深度遍历,且父组件的变更会彻底更新子组件,因此理论上key值对于非兄弟组件的做用,就显得微乎其微。那么对于非兄弟组件,key值相同应该是可行的。那么用实验验证一下咱们的猜测。

// 变动前
    <Demo1 key={1}>1
        <Demo2 key={1}>2
            <Demo4 key={4}>4</Demo4>
            <Demo5 key={5}>5</Demo5>
            <Demo6 key={6}>6</Demo6>
        </Demo2>
        <Demo3 key={2}>3
            <Demo4 key={4}>4</Demo4>
            <Demo5 key={5}>5</Demo5>
            <Demo6 key={6}>6</Demo6>
        </Demo3>
    </Demo1>
    // 变动后
    <Demo1 key={1}>1
        <Demo2 key={1}>2
            <Demo5 key={5}>5</Demo5>
            <Demo6 key={6}>6</Demo6>
        </Demo2>
        <Demo3 key={2}>3
            <Demo4 key={4}>4</Demo4>
            <Demo6 key={6}>6</Demo6>
        </Demo3>
    </Demo1>

在这个实验中,组件1和组件2有着相同的key值,且组件2和组件3的子组件也有着相同的key值,然而运行该代码,却并无关于key值相同的警告。执行Dom变动后,日志输出也同以前的猜测没有出入。可见咱们的猜测是正确的,key值并不是须要绝对惟一,只是须要保证在同一个父节点下的兄弟节点中惟一即可以了。

key的更多用法

除了上面提到的这些以外,在了解了key的做用机制以后,还能够利用key值来实现一些其它的效果。好比能够利用key值来更新一个拥有自状态的组件,经过修改该组件的key值,即可以达到使该组件从新渲染到初始状态的效果。此外,key值除了在列表中使用以外,在任何会操做dom,好比新增,删除这种影响兄弟节点顺序的状况,均可以经过添加key值的方法来提升渲染的效率。

相关文章
相关标签/搜索