react学习(八) diff 算法实现

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

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

事例分析

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

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

diff 特点

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

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

diff 思路

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

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

  1. 循环新数组
  2. 先出 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

  1. 是用原生库打印,看结构

可以发现就是一个简单的 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 新的生命周期。

原文地址:https://cloud.tencent.com/developer/article/2008498

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom