x-note
  • Introduction
  • JavaScript
    • JavaScript 作用域链
    • JavaScript 数据结构与类型
    • JavaScript 原型
    • JavaScript this 关键字
    • JavaScript 函数
    • JavaScript delete 运算符
    • JavaScript 内存管理与垃圾回收
    • JavaScript 严格模式与混乱模式
    • JavaScript 数字精度丢失
    • JavaScript 并发模型
    • 利用原型链实现继承
  • ECMAScript
    • ECMAScript 6 变量及常量的声明
    • ECMAScript 6 变量的解构赋值
    • ECMAScript 6 Promise 对象
    • ECMAScript 6 Symbol
    • ECMAScript 6 Proxy
    • ECMAScript 6 Reflect
    • ECMAScript 6 new.target
    • ECMAScript 6 Set 和 WeakSet
    • ECMAScript 6 Map 和 WeakMap
    • ECMAScript 6 Iterator
    • ECMAScript 6 Generator
    • ECMAScript 6 class
    • ECMAScript 7
    • ECMAScript 8 async 函数
    • ECMAScript 8 内存共享与原子性
    • ECMAScript 8 Others
    • ECMAScript 2018
    • ECMAScript 2019
  • CSS
    • CSS 块格式化上下文(BFC)
    • CSS 盒模型
    • CSS 外边距合并
    • CSS Float
    • CSS Position
    • CSS Border-Image
    • CSS BEM
    • CSS 表布局详解
    • 页面布局之单列布局
    • 页面布局之多列布局
  • React
    • React 组件的生命周期
    • React 虚拟 DOM
    • React Reconciliation
    • React Diff 算法核心
    • React Fiber
    • React Scheduling
    • React Context API
    • React Refs
    • React HMR
    • React Hook
  • VUE
    • VUE 响应式系统
    • VUE 渲染机制
    • 关于 Vue 的思考
  • Webpack
    • Webpack 基本概念
    • Webpack HMR
  • Babel
    • @babel/preset-env
  • WEB
    • WEB 基础知识及概念
      • 屏幕测量单位
      • 重绘与重排
      • 前端模块化系统
      • WEB 客户端存储
      • 浏览器的渲染过程
    • WEB 性能优化
      • WEB 性能指标
      • WEB 图片优化
      • 懒加载资源
    • WEB 安全
      • XSS
      • XSRF
      • 点击劫持
      • 同源策略(Same Origin Policy,SOP)
    • WEB 解决方案
      • webp 兼容方案
      • WEB 拖拽实现方案
    • WEB SEO
  • Git
    • Git 工作流
    • Git 内部原理
  • 传输协议
    • UDP
      • UDP 基本概念
    • TCP
      • TCP 基本概念
    • HTTP
      • HTTP 基础
      • HTTP 缓存
      • HTTP-2
      • HTTP-3
      • HTTPS
      • 自定义 HTTPS 证书
  • Protocol Buffers
    • Protocol Buffers 基础
  • gRPC
    • gRPC 简介
    • gRPC 基础概念
    • GRPC with GraphQL and TypeScript
  • 正则表达式
    • 正则表达式基础
    • 正则表达式的悲观回溯
  • 基础算法
    • 冒泡排序
    • 插入排序
    • 选择排序
    • 快速排序
    • 归并排序
    • 希尔排序
    • 堆排序
    • 桶排序
    • 计数排序
    • 基数排序
    • 二叉树的遍历
    • 动态规划
    • 回溯
  • 压缩算法
    • HPACK
    • QPACK
  • 设计模式
    • DDD
      • 模型元素的模式
    • 常见设计模式
      • 工厂方法
      • 抽象工厂
      • 构造器
      • 原型
      • 单例模式
      • 适配器模式
      • 桥接模式
      • 组合模式
      • 外观模式
      • 享元模式
      • 代理模式
      • 责任链模式
      • 命令模式
      • 迭代器模式
      • 中介者模式
      • 备忘录模式
      • 观察者模式
      • 状态模式
      • 策略模式
      • 模版方法模式
      • 访问者模式
      • 依赖注入
    • MVC
    • MVP
    • MVVM
  • 颜色空间
    • LCH
由 GitBook 提供支持
在本页
  • Diff 算法核心
  • 不同类型的 React Element
  • 同类型的 DOM Element
  • 同类型的 React Element
  • 递归子节点
  • React Key
  • 参考
在GitHub上编辑
  1. React

React Diff 算法核心

Diff 算法,即差异比较算法,是 React 实现原理的核心之一,React 通过此算法比较每次更新之后子虚拟 DOM 树种最小的差异点,并进行计算。

首先观察 React 是如何工作的。

假设有一个组件MyComponent,长这样

class MyComponent extends React.Component {
  render() {
    const { isFirst } = this.props;
    if (isFirst) {
      return (
        <div className="first">
          <span>A Span</span>
        </div>
      );
    }
    return (
      <div className="second">
        <p>A Paragraph</p>
      </div>
    );
  }
}

我们希望执行以下的行为:

  1. 首先挂载<MyComponent isFirst={true} />

  2. 然后用<MyComponent isFirst={flase} />替换<MyComponent isFirst={true} />

  3. 卸载这个组件

将这样的一个过程转换成 DOM 指令:

从没有到第一步:

创建一个节点

<div className="first">
  <span>A Span</span>
</div>
;

从第一步到第二步:

替换属性className="first"为className="seconde". 替换节点<span>A Span</span>为<p>A paragraph</p>

从第二步到卸载组件:

删除节点

<div className="second">
  <p>A Paragraph</p>
</div>

Diff 算法核心

区分两棵树时,React 首先比较两个根元素。行为因根元素的类型而异。

  1. 不同类型的两个元素将产生不同的树。

  2. 开发人员可以使用 props key 提示哪些子元素在不同渲染中可以保持稳定。

不同类型的 React Element

每当根元素具有不同类型时,React 都会拆开旧树并从头开始构建新树。从 <a> 到 <img>,或从 <Article> 到 <Comment>,或从 <Button> 到 <div> -任何这些都将导致完全重建。

拆除树时,旧的 DOM 节点将被破坏。 组件实例接收 componentWillUnmount()。 建立新树时,会将新的 DOM 节点插入到 DOM 中。 组件实例接收 componentWillMount(),然后接收 componentDidMount()。 与旧树关联的任何状态都将丢失。 根目录下的所有组件也将被卸载并破坏其状态。

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

这将销毁旧的 Counter,然后重新安装新的 Counter。

同类型的 DOM Element

比较两个相同类型的 React DOM 元素时,React 会查看两者的属性,保留相同的基础 DOM 节点,并仅更新更改的属性。

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

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

通过比较这两个元素,React 知道只修改底层 DOM 节点上的 className。

在更新样式时,React 还知道仅更新已更改的属性。

<div style={{color: 'red', fontWeight: 'bold'}} />

<div style={{color: 'green', fontWeight: 'bold'}} />

在这两个元素之间进行转换时,React 知道仅修改颜色样式,而不修改 fontWeight。

处理完 DOM 节点后,React 然后在子节点上递归。

同类型的 React Element

组件更新时,实例保持不变,因此在渲染之间保持状态。 React 更新基础组件实例的属性以匹配新元素,并在基础实例上调用 componentWillReceiveProps()和 componentWillUpdate()。

接下来,调用 render()方法,并且 diff 算法根据先前的结果和新的结果进行递归。

递归子节点

默认情况下,在 DOM 节点的子节点上递归时,React 只会同时遍历两个子节点列表,并在存在差异时生成一个更新操作。

例如,在子元素的末尾添加元素时,在这两个树之间进行转换会很好:

<ul>
  <li>first</li>
  <li>second</li>
</ul>
// to
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

React 会匹配两个 <li>first</li> 树,两个 <li>second</li> 树,然后插入一个 <li>third</li> 树。(这看起来很不错)

但是,对于下列例子则会导致糟糕的性能。

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>
// to
<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

React 会操作每个子节点,而不是使<li>Duke</li> 和 <li>Villanova</li>保持不变,并在前面插入 <li>Connecticut</li>

React 中渲染多个组件如果不包含 key 属性,你将会在控制面板看到这个警告 "Warning: Each child in an array or iterator should have a unique 'key' prop.

React Key

React 支持 key 属性。当子项具有 key 时,React 使用该键将原始树中的子项与后续树中的子项进行匹配。

在对子项进行 diff 时,存在三种类型的操作:

  • MOVE_EXISTING -- 存在相同的节点则复用以前的 DOM 节点,做移动操作。

  • INSERT_MARKUP -- 新的节点不在旧集合里则插入新的节点。

  • REMOVE_NODE -- 新集合里在旧集合中对应的 node 不同,不能直接复用和更新,需要执行删除操作,或者旧集合中的节点不在新集合里的。

  1. 遍历 newChildrens,基于 key 判断 newChild 是否在 oldChildrens 存在相同的节点.

    1. 如果存在相同节点(prevChild === nextChild)

      1. 判断原先节点的变化顺序(不考虑头部新插入的节点)

        1. 节点的挂载顺序变大(从前往后),移动节点

        2. 节点的挂载顺序变小(从后往前或不变),不做操作

      2. 节点的 _mountIndex 变为新集合中的 index

    2. 如果不存在相同节点,

      1. 之前存在相同

      2. 在上一个新集合中的节点后插入新节点

  2. 遍历 oldChildrens,移除在新集合中不存在的节点 (???)

// befor
<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
  <li key="2017">Alex</li>
  <li key="2018">Frank</li>
</ul>

// after
<ul>
  <li key="2014">Connecticut</li>
  <li key="2018">Frank</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

对与上述例子 DOM 节点的变化顺序为

// step 1
// 插入新的节点 `<li key="2014">Connecticut</li>`
<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
  <li key="2017">Alex</li>
  <li key="2018">Frank</li>
</ul>

// step 2
// key 2018 向前移动,实际上不做操作
<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
  <li key="2017">Alex</li>
  <li key="2018">Frank</li>
</ul>

// step 3
// `<li key="2015">Duke</li>` 移动至 `<li key="2018">Frank</li>` 后
<ul>
  <li key="2014">Connecticut</li>
  <li key="2016">Villanova</li>
  <li key="2017">Alex</li>
  <li key="2018">Frank</li>
  <li key="2015">Duke</li>
</ul>

// step 4
// `<li key="2016">Villanova</li>` 移动至 `<li key="2015">Duke</li>` 后
<ul>
  <li key="2014">Connecticut</li>
  <li key="2017">Alex</li>
  <li key="2018">Frank</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

// step 5
// 移除新集合中不存在的节点 `<li key="2017">Frank</li>`
<ul>
  <li key="2014">Connecticut</li>
  <li key="2018">Frank</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

所以不得已时,您可以将数组中项目的索引作为键传递。但是使用索引作为 key 式,重新排序会很慢。

参考

上一页React Reconciliation下一页React Fiber

最后更新于4年前

找到两棵任意的树之间的最小的差异是一个复杂度为 O(n3)O(n^3)O(n3)的问题,React Diff 算法根据 Web 应用的特点 (Web 应用很少有 component 移动到树的另一个层级,它们大部分只是在相邻的子节点之间移动) 作出以下两个假设,最终达到了接近 O(n)O(n)O(n) 的复杂度:

How React Compare two tree
Rerender With Key

React Render with Key in React v15
使用 Key 作为索引可能会导致的问题
不使用索引作为键如何解决这些重新排序,排序和前置问题
React’s diff algorithm
官方文档
React 源码剖析系列 - 不可思议的 react diff