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

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

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

    事例分析

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

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

    按照上面图的结构,我们需要知道那个元素变化了,其实右边相对左边只是把 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 中没有值,需要新增;新的数组节点循环完,未用到的老节点全部删除。

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

    实现 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

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

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

    可以发现就是一个简单的 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

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

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

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

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

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

    声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。