react diff 算法实现思路及原理解析

目录
  • 事例分析
    • diff 特点
    • diff 思路
  • 实现 diff 算法
    • 修改入口文件
    • 实现 React.Fragment
  • 我们需要修改 children 对比

前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有我们常见的 key 的作用。

diff 算法使用在子都是数组的情况下,这点和 vue 是一样的。如果元素是其他类型的话直接替换就好。

事例分析

按照之前的 diff 写法,如果元素不同我们是直接删了 a 再插入的:

按照上面图的结构,我们需要知道那个元素变化了,其实右边相对左边只是把 A 做了移动,没有 dom 元素的删除和新增。

diff 特点

  • 同级对比 On
  • 类型不一样销毁老的,创建新的
  • 通过 key 标识

key 这里需要标识,主要是为了列表中有删除新增时有优化效果,如果纯静态列表,只是展示作用,key 意义不大。

diff 思路

  • 使用 map 存储节点状态,格式如下:
let map = {
  keyA: ADOM,
  keyB: BDOM
}
  • 定义 lastPlacedIndex 记录上一个不需要移动的老节点

默认 lastPlacedIndex = 0 ,上一个不需要移动的节点,在循环新的子虚拟 dom 时,如果老节点的挂载索引小于当前值,则改变 lastPlacedIndex。这里有点类似 vue 的最长递增子序列,最大的保证不变的 dom 元素,只是判断方式不同。

  • 循环新数组
  • 先出 Amap 中如果有 A,表示可以复用

    • 判断 A 的老挂载索引和 lastPlacedIndex 对比,如果索引值大,A 节点不需要移动,更新 lastPlacedIndex 的值;否则循环到 B,挂载索引小,需要移动 B;循环到 Gmap 中没有值,需要新增;新的数组节点循环完,未用到的老节点全部删除。

实现 diff 算法

修改入口文件

// src/index.js
class Counter extends React.Component {
  constructor(props) {
    super(props)
    this.state = {list: ['A','B', 'C', 'D', 'E', 'F']}
  }
  handleClick = () => {
    this.setState({
      list: ['A', 'C', 'E', 'B', 'G']
    })
  }
  render() {
    // 使用空标签
    return <React.Fragment>
      <ul>
      {this.state.list.map(item => {
        // 这里使用 key 标识
        return <li key={item}>{item}</li>
      })}
      </ul>
      <button onClick={this.handleClick}>add 1</button>
    </React.Fragment>
  }
}

实现 React.Fragment

Fragment 就是代码片段,不占用 dom 结构。简写 <></>,对应 dom 操作为 createDocumentFragment

  • 是用原生库打印,看结构

可以发现就是一个简单的 Symbol,所以需要定义新的类型:

为什么一个简单的 Symbol 可以被渲染成片段呢?依赖于 babel 解析。

// src/constants.js
export const REACT_FRAGMENT = Symbol("react.fragment") // React.Fragment 标签
// 备用,diff 时做 patch 的 type 定义
// 新的插入
export const PLACEMENT = 'PLACEMENT'
// 复用的移动
export const MOVE = 'MOVE'

在创建元素的时候进行类型判断,记得 react.js 中导出

// src/react-dom.js
// createDOM 方法
else if (type === REACT_FRAGMENT) {
  // fragment 片段
  dom = document.createDocumentFragment()
}
// updateElement 方法
else if (oldVdom.type === REACT_FRAGMENT) {
  // fragment 不需要对比,直接对比 子 就可以了
  const currentDOM = newVdom.dom = findDOM(oldVdom)
    updateChildren(currentDOM, oldVdom.props.children, newVdom.props.children)
}

我们需要修改 children 对比

之前逻辑:

// src/react-dom.js
// diff  没有做复用,直接做的替换
function updateChildren(parentDOM, oldVChildren, newVChildren) {
  // 拿到最长的
  let maxLength = Math.max(oldVChildren.length, newVChildren.length);
  for (let i = 0; i < maxLength; i++) {
  // 不能直接 appendChild 进父,需要找到当前操作的节点的下一个节点。在其前面插入
    const nextVdom = oldVChildren.find((item, index) => index > i && item && findDOM(item))
    compareTwoVdom(parentDOM, oldVChildren[i], newVChildren[i], findDOM(nextVdom));
  }
}

新的逻辑(参考上面的流程):

// diff
function updateChildren(parentDOM, oldVChildren, newVChildren) {
  oldVChildren = Array.isArray(oldVChildren) ? oldVChildren : [oldVChildren];
  newVChildren = Array.isArray(newVChildren) ? newVChildren : [newVChildren];

 // 1.循环老结构, 构建map存储  key: dom
  const keydOldMap = {}
  let lastPlacedIndex = 0
  oldVChildren.forEach((oldVChild, index) => {
    let oldKey = oldVChild?.key || index //  写key 了就用key,没写默认 index
    keydOldMap[oldKey] = oldVChild
  })
  // 2. 创建 dom 补丁包,收集 dom 操作
  const patch = []
  newVChildren.forEach((newVChild, index) => {
    newVChild.mountIndex = index // 为新元素每个添加索引标识
    const newKey = newVChild?.key || index
    const oldVChild = keydOldMap[newKey] // 看有没有存
    if(oldVChild) {
      // 如果有老的,就去更新老节点 这里直接可以复用
      updateElement(findDOM(oldVChild).parentNode, oldVChild, newVChild)
      if(oldVChild.mountIndex < lastPlacedIndex) {
        patch.push({
          type: MOVE,
          oldVChild,
          newVChild,
          mountIndex: index // 旧的移动到新的的位置
        })
      }
      // 复用过了 删除掉
      delete keydOldMap[newKey]
      lastPlacedIndex = Math.max(lastPlacedIndex, oldVChild.mountIndex)// 取最大
    } else {
      // 新的
      patch.push({
        type: PLACEMENT,
        newVChild,
        mountIndex: index
      })
    }
  })
  // 找到需要移动的老节点
  const moveVChildren = patch.filter(action => action.type === MOVE).map(action => action.oldVChild)
  // 把要删除的节点 和  要移动的节点先全删除     (页面里没有了,但是内存中还存在  patch 中有存)
  Object.values(keydOldMap).concat(moveVChildren).forEach(oldVdom => {
    let currentDOM = findDOM(oldVdom)
    currentDOM.remove()
  })
  patch.forEach(action => {
    const {type, oldVChild, newVChild, mountIndex} = action
    // 老的真实子节点
    const childNodes = parentDOM.childNodes
    // 新的插入
    if (type === PLACEMENT) {
      let newDOM = createDOM(newVChild)
      let childNode = childNodes[mountIndex] // 老真实节点
      if (childNode) {
        // 往 老的父对应位置插入
        parentDOM.insertBefore(newDOM, childNode)
      } else {
        parentDOM.appendChild(newDOM)
      }
    } else if (type === MOVE) {
      // 移动不用创建 新 dom,复用
      let oldDOM = findDOM(oldVChild)
      let childNode = childNodes[mountIndex] // 老真实节点
      if (childNode) {
        // 往 老的父对应位置插入
        parentDOM.insertBefore(oldDOM, childNode)
      } else {
        parentDOM.appendChild(oldDOM)
      }
    }
  })
}

实现如下跟原生一致,可以看到,三个节点实现了复用,即 A, C, E

如果没有写 key,我们在看效果:

可以看到只有第一个节点实现了复用,因为默认索引都使用的 0。所以这也是为什么不建议我们使用索引当 key 的原因。动态列表 key 意义不大。

本节代码不是很多,主要是 diff 算法的思路和实现原理。如果了解了 vuediff 算法,相信理解起来更好,也能更好的对比。下一小节我们学习下 react 新的生命周期。

到此这篇关于react diff 算法实现思路及原理解析的文章就介绍到这了,更多相关react diff 算法内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入浅析React中diff算法

    React中diff算法的理解 diff算法用来计算出Virtual DOM中改变的部分,然后针对该部分进行DOM操作,而不用重新渲染整个页面,渲染整个DOM结构的过程中开销是很大的,需要浏览器对DOM结构进行重绘与回流,而diff算法能够使得操作过程中只更新修改的那部分DOM结构而不更新整个DOM,这样能够最小化操作DOM结构,能够最大程度上减少浏览器重绘与回流的规模. 虚拟DOM diff算法的基础是Virtual DOM,Virtual DOM是一棵以JavaScript对象作为基础的树,

  • react diff算法源码解析

    React中Diff算法又称为调和算法,对应函数名为reconcileChildren,它的主要作用是标记更新过程中那些元素发生了变化,这些变化包括新增.移动.删除.过程发生在beginWork阶段,只有非初次渲染才会Diff. 以前看过一些文章将Diff算法表述为两颗Fiber树的比较,这是不正确的,实际的Diff过程是一组现有的Fiber节点和新的由JSX生成的ReactElement的比较,然后生成新的Fiber节点的过程,这个过程中也会尝试复用现有Fiber节点. 节点Diff又分为两种

  • 浅谈从React渲染流程分析Diff算法

    React中最神奇的部分莫过于虚拟DOM,以及其高效的Diff算法.这让我们可以无需担心性能问题而"毫无顾忌"的随时"刷新"整个页面,由虚拟DOM来确保只对界面上真正变化的部分进行实际的DOM操作.React在这一部分已经做到足够透明,在实际开发中我们基本无需关心虚拟DOM是如何运作的.然而,理解其运行机制不仅有助于更好的理解React组件的生命周期,而且对于进一步优化React程序也会有很大帮助. 1.什么是虚拟DOM 在React中,render执行的结果得到的

  • React中的Diff算法你了解吗

    目录 一.Diff算法的作用 二.React的Diff算法 1.什么是调和? 2.什么是Reactdiff算法? 3.diff策略 4.treediff: 5.componentdiff: 6.elementdiff 三.基于Diff的开发建议 总结 一.Diff算法的作用 渲染真实DOM的开销很大,有时候我们修改了某个数据,直接渲染到真实dom上会引起整个dom树的重绘和重排.我们希望只更新我们修改的那一小块dom,而不是整个dom,diff算法就帮我们实现了这点. diff算法的本质就是:找

  • react中的虚拟dom和diff算法详解

    虚拟DOM的作用 首先我们要知道虚拟dom的出现是为了解决什么问题的,他解决我们平时频繁的直接操作DOM效率低下的问题.那么为什么我们直接操作DOM效率会低下呢? 比如我们创建一个div,我们可以在控制台查看一下这个div上自带或者继承了很多属性,尤其是我们使用js操作DOM的时候,我们的DOM本身就很复杂,js的操作也会占用很多时间,但是我们控制不了DOM元素本身,因此虚拟DOM解决的是js操作DOM这一层面,其实解决的是减少了操作dom的次数 简单实现虚拟DOM 虚拟DOM,见名知意,就是假

  • React diff算法的实现示例

    前言 在上一篇文章,我们已经实现了React的组件功能,从功能的角度来说已经实现了React的核心功能了. 但是我们的实现方式有很大的问题:每次更新都重新渲染整个应用或者整个组件,DOM操作十分昂贵,这样性能损耗非常大. 为了减少DOM更新,我们需要找渲染前后真正变化的部分,只更新这一部分DOM.而对比变化,找出需要更新部分的算法我们称之为diff算法. 对比策略 在前面两篇文章后,我们实现了一个render方法,它能将虚拟DOM渲染成真正的DOM,我们现在就需要改进它,让它不要再傻乎乎地重新渲

  • react diff 算法实现思路及原理解析

    目录 事例分析 diff 特点 diff 思路 实现 diff 算法 修改入口文件 实现 React.Fragment 我们需要修改 children 对比 前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有我们常见的 key 的作用. diff 算法使用在子都是数组的情况下,这点和 vue 是一样的.如果元素是其他类型的话直接替换就好. 事例分析 按照之前的 diff 写法,如果元素不

  • C/C++实现快速排序算法的思路及原理解析

    快速排序 1. 算法思想 快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序. 2. 实现原理 2.1.设置两个变量 low.high,排序开始时:low=0,high=size-1. 2.2.整个数组找基准正确位置,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面 默认数组的第一个数为基准数据,赋值给key,即key=array[low]. 因为默认数组的第一个

  • React diff算法原理详细分析

    目录 抛砖引玉 传统diff算法 React diff原理 tree diff component diff element diff 应用实践 页面指定区域刷新 更加方便地监听props改变 react-router中Link问题 结语 抛砖引玉 React通过引入Virtual DOM的概念,极大地避免无效的Dom操作,已使我们的页面的构建效率提到了极大的提升.但是如何高效地通过对比新旧Virtual DOM来找出真正的Dom变化之处同样也决定着页面的性能,React用其特殊的diff算法解

  • React Diff算法不采用Vue的双端对比原因详解

    目录 前言 React 官方的解析 Fiber 的结构 Fiber 链表的生成 React 的 Diff 算法 第一轮,常见情况的比对 第二轮,不常见的情况的比对 重点如何协调更新位置信息 小结 图文解释 React Diff 算法 最简单的 Diff 场景 复杂的 Diff 场景 Vue3 的 Diff 算法 第一轮,常见情况的比对 第二轮,复杂情况的比对 Vue2 的 Diff 算法 第一轮,简单情况的比对 第二轮,不常见的情况的比对 React.Vue3.Vue2 的 Diff 算法对比

  • React不使用requestIdleCallback实现调度原理解析

    目录 1.起因 2.查找问题 3.解决问题 4.总结 5.吐槽 1.起因 最近在一边啃源码,一边手写fiber嘛,然后也看了很多博客和资料,基本上大伙好像都是说用requestIdleCallback来模拟react实现一个空闲时间调度.但我自己手写的时候把怎么用怎么怪,老是感觉有什么地方不对劲而且是在调度过程中,可能是因为我是想写出来来一个相对健全一点的模版方便我以后写源码的其他部分把,然后分析了一下所以有了这篇博客. 2.查找问题 1.requestIdleCallback是利用帧之间空闲时

  • React setState是异步还是同步原理解析

    目录 setState异步更新 那么为什么setState设计为异步呢? 如何获取异步的结果 setState一定是异步的吗? setState异步更新 开发中当组件中的状态发生了变化,页面并不会重新渲染.我们必须要通过setState来告知React数据已经发生了变化,重新渲染页面. 先来看下面的例子: constructor() { super(); this.state = { message: "Hello World", }; } changeText() { this.se

  • React实现核心Diff算法的示例代码

    目录 Diff算法的设计思路 Demo介绍 Diff算法实现 遍历前的准备工作 遍历after 遍历后的收尾工作 总结 Diff算法的设计思路 试想,Diff算法需要考虑多少种情况呢?大体分三种,分别是: 节点属性变化,比如: // 更新前 <ul> <li key="0" className="before">0</li> <li key="1">1</li> </ul>

  • React的diff算法核心复用图文详解

    目录 引言 Fiber 架构 React 的 diff 算法 总结 引言 React 是基于 vdom 的前端框架,组件 render 产生 vdom,然后渲染器把 vdom 渲染出来. state 更新的时候,组件会重新 render,产生新的 vdom,在浏览器平台下,为了减少 dom 的创建,React 会对两次的 render 结果做 diff,尽量复用 dom,提高性能. diff 算法是前端框架中比较复杂的部分,代码比较多,但今天我们不上代码,只看图来理解它. 首先,我们先过一下 r

随机推荐