React 虚拟DOM中的Diffing算法

@一棵菜菜  June 3, 2019

React原理

在某一时间节点调用 React 的 render() 方法,会创建一棵由 React 元素组成的树(虚拟DOM)
在下一次 state 或 props 更新时,相同的 render() 方法会返回一棵不同的树。
React 需要基于这两棵树之间的差别来判断如何有效率的更新 UI 以保证当前 UI 与最新的树保持同步。难点在于如何判断新旧两个 JS 对象的最小差异并且实现局部更新 DOM

什么是虚拟DOM?——就是一个JS对象,用来描述真实的DOM。
更多学习:查看我的文章《React 中的Virtual DOM》

通过 JS 来模拟 DOM(虚拟dom):

const ul = {
  tag: 'ul',
  props: {
    class: 'list'
  },
  children: {
    tag: 'li',
    children: '1'
  }
}

上述代码对应的 DOM 就是

<ul class='list'>
  <li>1</li>
</ul>

Diffing 算法

即对两个虚拟DOM做比对时的算法。同级比较。

当对比两颗树时,React 首先比较两棵树的根节点。不同类型的根节点元素会有不同的形态。

1. 比对不同类型的React元素

当根节点为不同类型的元素时,React 会卸载原有的树并且建立起新的树。

  • 当卸载一颗树时,对应的 DOM 节点也会被销毁。
  • 当建立一颗新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中。
  • (在根节点以下的组件也会被卸载,它们的状态会被销毁。)
<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

2. 比对同一类型的React元素

React 会保留 DOM 节点,仅比对及更新有改变的属性。

<div className="before" title="stuff" />
<div className="after" title="stuff" />

3. 比对同类型的组件元素

当一个组件更新时,组件实例保持不变,这样 state 在跨越不同的渲染时保持一致。
React 将更新该组件实例的 props 以跟最新的元素保持一致。
下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归

对子节点进行递归

keys

组件实例是基于它们的 key 来决定是否更新以及复用。
能不用indexkey值就不要用!

案例分析:

const list = [{ id: 0, val: "a" }, { id: 1, val: "b" }, { id: 2, val: "c" }];

用index做key值的问题

react生成虚拟DOM并渲染真实DOM。

遍历渲染值为val: a  b  c
key值为index:   0  1  2

list删除第一项,并setState(),则react生成新的虚拟DOM,并与老的虚拟DOM比对,发现第key=0的值变成了b,key=1的值变成了c,key=2的项没有了。则生成真实DOM时是分别修改值key为0、1的值等。

遍历渲染值为val: b  c
key值对应为id:  0  1

用表内的唯一值做key【非常推荐】

react生成虚拟DOM并渲染真实DOM。

遍历渲染值为val: a  b  c
key值对应为id:  0  1  2

list删除第一项,并setState(),则react生成新的虚拟DOM,并与老的虚拟DOM比对,发现是key为0的项没有了,则生成真实DOM时删除key=0第一个节点即可。

遍历渲染值为val: b  c
key值对应为id:  1  2

注意

1.该算法不会尝试匹配不同组件类型的子树如果你发现你在两种不同类型的组件中切换,但输出非常相似的内容,建议把它们改成同一类型。在实践中,我们没有遇到这类问题。
2.Key 应该具有稳定,可预测,以及列表内唯一的特质。
更多请查看官方文档《协调》

其他文章摘抄

首先 DOM 是一个多叉树的结构,如果需要完整的对比两颗树的差异,那么需要的时间复杂度会是 O(n ^ 3),这个复杂度肯定是不能接受的。于是 React 团队优化了算法,实现了 O(n) 的复杂度来对比差异。
实现 O(n) 复杂度的关键就是只对比同层的节点,而不是跨层对比,这也是考虑到在实际业务中很少会去跨层的移动 DOM 元素。 所以判断差异的算法就分为了两步:

  • 树的深度遍历:首先从上至下,从左往右遍历对象。这一步中会给每个节点添加索引,便于最后渲染差异
  • 一旦节点有子元素,就去判断子元素是否有不同。

在第一步算法中我们需要判断新旧节点的 tagName 是否相同,如果不相同的话就代表节点被替换了。如果没有更改 tagName 的话,就需要判断是否有子元素,有的话就进行第二步算法。

在第二步算法中,我们需要判断原本的列表中是否有节点被移除,在新的列表中需要判断是否有新的节点加入,还需要判断节点是否有移动。

查看原文部分《Virtual DOM》
关于React 的深度优先遍历 推荐阅读《深入理解react中的虚拟DOM、diff算法》

摘抄(需整理)

强烈推荐:React 源码剖析系列 - 不可思议的 react diff

  1. react官方建议不要进行DOM节点跨层级操作

既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。【重点】当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。

 React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作

竹子为什么会选择vue而没有选择react:

  1. react的diff算法不合适:编辑器内会有频繁的拖拽、移动元素的操作,比如位于根节点下的一个图片组件A被拖放到很深层的一个节点内。但是react只会简单地考虑同层级节点的位置移动,对于不同层级的节点,react只会进行删除和新建的操作。当根节点发现子节点中组件A消失了,就会直接销毁A;当深层节点发现多了子节点A,则会创建新的A(包括A内的所有子节点)作为其子节点。

所以:当出现节点跨层级移动时,并不会出现想像中的移动操作,而是以A为根节点的树先被删除再被重新创建。这是一种影响react性能的操作,因此react官方建议不要进行DOM节点跨层级操作

在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。

  1. 如果你发现你在两种不同类型的组件中切换,但输出非常相似的内容,建议把它们改成同一类型!

如果两个 component 是不同类型但结构相似时,React diff 会影响性能,因为该算法不会尝试匹配不同组件类型的子树。

节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入,新建)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。

移动:可以复用以前的 DOM 节点

写博客的原因或好处:
分享我的技术心得;自己做技术总结;好记性不如烂笔头,方便自己快速查阅和回顾。


content1:
<div>
  菜菜
  <article>
    <h1>标题</h1>
  </article>
</div>


content2:
<div>
  石头
  + <p>满足条件时插入一个p标签</p>
  <article>
    <h1>标题</h1>
  </article>
</div>


content3:
<div>
  石头
  <p>满足条件时插入一个p标签</p>
  <div>
    <article>
      <h1>标题</h1>
    </article>
  </div>
</div>


content4:
// (react非常不建议这样写,所以平时基本上不会遇到这种情况去对比)
<section>
  石头
  <p>插入一个p标签</p>
  <article>
    <h1>标题</h1>
  </article>
</section>



content5:
<header>变成了头部</header>

// 虚拟DOM
const content1 = {
  tagName: 'div',
  props: {},
  children: [
    '菜菜',
    {
      tagName: 'article',
      props: {},
      children: [
        { tagName: 'h1', props: {}, children: ['标题'] }
      ]
    }
  ]
}

const content2 = {
  tagName: 'div',
  props: {},
  children: [
    '石头',
    {
      tagName: 'p',
      props: {},
      children: [
        '满足条件时插入一个p标签'
      ]
    },
    {
      tagName: 'article',
      props: {},
      children: [
        { tagName: 'h1', props: {}, children: ['标题'] }
      ]
    }
  ]
}

// 1. 比对不同类型的React元素(父节点tagName)
// 2. 比对同一类型的React元素
// 3. 比对同类型的组件元素

const content3 = {
  tagName: 'div',
  props: {},
  children: [
    '石头',
    {
      tagName: 'p',
      props: {},
      children: [
        '满足条件时插入一个p标签'
      ]
    },
    {
      tagName:'div',// article 包了一层div
      props:{},
      children:[
        {
          tagName: 'article',
          props: {},
          children: [
            { tagName: 'h1', props: {}, children: ['标题'] }
          ]
        }
      ]
    }
  ]
}

const content4 = {
  tagName: 'section',
  props: {},
  children: [] // children 同content2
}



const content5 = {
  tagName: 'header',
  props: {},
  children: ['变成了头部']
}

添加新评论