问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

React中的虚拟DOM和diff算法详解

创作时间:
作者:
@小白创作中心

React中的虚拟DOM和diff算法详解

引用
CSDN
1.
https://blog.csdn.net/m0_58782068/article/details/145455191

React中的虚拟DOM和diff算法是其高效更新UI的核心机制。虚拟DOM通过在内存中对比差异,最小化对真实DOM的操作,提高了应用的性能和响应速度。本文将详细介绍虚拟DOM的工作原理、优势以及diff算法的三大策略。

虚拟DOM

结构对比

虚拟DOM实际上是一个对象的结构,React通过该对象将真实DOM渲染出来。真实的DOM上会挂载很多默认的属性和方法,而虚拟DOM上并没有这些属性。由于我们实际上不需要关注这些属性,因此虚拟DOM比真实DOM更轻,运行起来更快。

流程对比

传统的DOM结构,只要数据发生变化就会实时地更新到用户界面中,这会导致频繁的DOM渲染,产生很大的性能消耗。而虚拟DOM则将所有的操作聚集到一块,计算出所有的变化后,统一更新一次虚拟DOM,再触发页面的更新,整个过程页面只重新渲染一次。

从上述描述可以看出,越是复杂的页面,或者DOM操作频繁的情况下,虚拟DOM的优势就越大。

虚拟DOM的主要部分

对于下面这段结构:

<div className='Index'>
  <div>虚拟 dom</div>
  <ul>
    <li>React</li>
  </ul>
</div>

在React转化为一个虚拟DOM对象:

{
    type: 'div',
    props: { class: 'Index' },
    children: [
        {
            type: 'div',
            children: '虚拟 dom'
        },
        {
            type: 'ul',
            children: [
                {
                    type: 'li',
                    children: 'Vue'
                },
            ]
        }
    ]
}

虚拟DOM主要包含三部分:

  • type:实际的标签
  • props:标签内部的属性(除key和ref,它们会形成单独的数据结构进行处理)
  • children:节点内容,依次循环

可以看出虚拟DOM的结构非常简单清晰。

虚拟DOM的优势

提高效率

使用原生JS时,我们需要关注如何操作DOM,怎样更新DOM。而React通过虚拟DOM确保DOM的匹配,让我们可以更加关注业务逻辑,从而提高开发效率。

性能提升

React会将整个DOM保存为虚拟DOM,如果有更新,都会维护两个虚拟DOM,以此来比较之前的状态和当前的状态,并确定哪些状态被修改,然后将这些变化更新到实际DOM上。一旦真正的DOM发生改变,也会更新UI。

在React源码中,createElement函数是创建虚拟DOM节点的关键:

// 创建一个 React 元素(虚拟 DOM 节点)的函数
function createElement(type, config, children) {
  // 用于遍历 config 对象属性的变量
  let propName; 
  // 用于存储 React 元素属性的对象
  const props = {}; 
  // 用于存储 React 元素的 key
  let key = null; 
  // 用于存储 React 元素的 ref
  let ref = null; 
  // 如果传入了 config 对象
  if (config!= null) { 
    // 如果 config 对象中有有效的 ref
    if (hasValidRef(config)) { 
      // 将 config 对象中的 ref 赋值给 ref 变量
      ref = config.ref; 
    }
    // 如果 config 对象中有有效的 key
    if (hasValidKey(config)) { 
      // 将 config 对象中的 key 转换为字符串并赋值给 key 变量
      key = '' + config.key; 
    }
    // 遍历 config 对象的所有可枚举属性
    for (propName in config) { 
      // 如果 config 对象自身拥有该属性,并且该属性不在 RESERVED_PROPS 对象中(RESERVED_PROPS 可能包含一些 React 保留的属性名)
      if (
        hasOwnProperty.call(config, propName) &&
        (!RESERVED_PROPS.hasOwnProperty(propName))
      ) {
        // 将 config 对象中的属性添加到 props 对象中
        props[propName] = config[propName]; 
      }
    }
  }
  // 计算传入的子元素数量
  const childrenLength = arguments.length - 2; 
  // 如果只有一个子元素
  if (childrenLength === 1) { 
    // 将唯一的子元素直接赋值给 props.children
    props.children = children; 
  } 
  // 如果有多个子元素
  else if (childrenLength > 1) { 
    // 创建一个长度为子元素数量的数组
    const childArray = Array(childrenLength); 
    // 遍历每个子元素,将其依次存入数组
    for (let i = 0; i < childrenLength; i++) { 
      childArray[i] = arguments[i + 2]; 
    }
    props.children = childArray; 
  }
  // 返回一个描述 React 元素的对象
  return {
    $$typeof: REACT_ELEMENT_TYPE, 
    type: type, 
    key: key, 
    ref: ref, 
    props: props, 
    ......
  };
}

这个函数将组件的类型(比如 div、span 或者自定义组件)、属性以及子元素等信息组合成一个虚拟DOM节点对象。React通过递归调用createElement,把整个组件树构建成一个完整的虚拟DOM树。

在React中,需要维护两个完全独立的虚拟DOM,在每次更新时,生成一个新的虚拟DOM树,然后和之前的虚拟DOM树进行对比。这一对比过程主要由协调算法(reconciliation)完成。在React中主要通过reconcileChildFibers函数协调:

function reconcileChildFibers(
  // 当前正在处理的父 Fiber 节点
  returnFiber,
  // 当前父节点的第一个子 Fiber 节点
  currentFirstChild,
  // 新的子节点
  newChild,
  // 优先级相关的概念,用于标记任务的优先级
  lanes
) {
  // 检查 newChild 是否是一个顶层无 key 的片段。如果是,则将 newChild 替换为其 props.children,当作普通数组处理
  if (newChild === null) {
    if (currentFirstChild!== null) {
      newChild = newChild.props.children;
    }
    return null;
  }
  // 如果 newChild 是一个对象,根据 $$typeof 属性的值进行不同处理。如果当前值不满足已有条件,调用 throwOnInvalidObjectType 抛出错误。
  if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          // 放置该子节点
          return placeSingleChild(
            // 处理单个元素
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        case REACT_PORTAL_TYPE:
          return placeSingleChild(
            // 处理单个门户
            reconcileSinglePortal(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        case REACT_LAZY_TYPE:
          // 懒加载组件,获取其 payload 和 init 函数,调用 init(payload) 得到实际的子节点,然后递归调用 reconcileChildFibers 处理
          const payload = newChild._payload;
          const init = newChild._init;
          return reconcileChildFibers(
            returnFiber,
            currentFirstChild,
            init(payload),
            lanes,
          );
      }
      // 数组
      if (isArray(newChild)) {
        return reconcileChildrenArray(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        );
      }
      // 迭代器
      if (getIteratorFn(newChild)) {
        return reconcileChildrenIterator(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        );
      }
      throwOnInvalidObjectType(returnFiber, newChild);
    }
   // 处理字符串和数字类型的新子节点
   if (
      (typeof newChild === 'string' && newChild !== '') ||
      typeof newChild === 'number'
    ) {
      return placeSingleChild(
        reconcileSingleTextNode(
          returnFiber,
          currentFirstChild,
          '' + newChild,
          lanes,
        ),
      );
    }
  // 一系列对比和更新逻辑
  //...
  // 对于其他未处理的情况,将剩余的子节点当作空处理,调用 deleteRemainingChildren 删除当前父节点下剩余的子节点。
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}

这个函数会遍历新旧虚拟DOM树的节点,比较它们的差异,确定哪些节点发生了变化。并会确定哪些状态被修改,然后将这些变化更新到实际DOM上。在找出虚拟DOM树之间的差异后,React会生成一系列的DOM更新操作,这些操作会被应用到实际的DOM上。一旦真正的DOM发生改变,也会更新UI。当React通过上述过程对真实DOM进行修改后,浏览器会自动检测到DOM树的变化,然后重新计算样式布局(回流和重绘),将最新的DOM状态展示到屏幕上,从而实现UI的更新。这是浏览器的原生机制,React依赖此机制确保页面随着数据变化实时更新。

总结来说,虚拟DOM和协调算法是React高效更新UI的核心机制,通过在内存中对比虚拟DOM差异,最小化对真实DOM的操作,提高了应用的性能和响应速度。

兼容性

React具有超强的兼容性,可分为:浏览器的兼容和跨平台兼容

  • React基于虚拟DOM实现了一套自己的事件机制,并且模拟了事件冒泡和捕获的过程,采取事件代理、批量更新等方法,从而磨平了各个浏览器的事件兼容性问题
  • 对于跨平台,React是根据虚拟DOM画出相应平台的UI层,只不过不同的平台画法不同而已

diff算法

diff算法就是通过循环递归对节点进行依次对比。在React主要通过三大策略完成了升级优化:

  1. Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。
  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
  3. 对于同一层级的一组子节点,它们可以通过唯一id进行区分

分别对应:

  • tree diff:同级比较,React通过updateDepth对VirtualDOM树进行层级控制,也就是同一层,在对比的过程中,如果发现节点不在了,会完全删除不会对其他地方进行比较,这样只需要对树遍历一次就OK了
  • component diff:组件比较:对同种类型组件对比,按照层级比较继续比较虚拟DOM树即可;对于不同组件来说,React会直接判定该组件为dirty component(脏组件),无论结构是否相似,只要判断为脏组件就会直接替换整个组件的所有节点
  • element diff:节点比较:React对于同一层级的一子自节点,通过唯一的key进行比较;
  • 在比较节点的时候,通过比较index(当前节点在旧节点中的位置)和lastIndex(对比的索引),lastIndex一开始是默认的0,当每次比较后,会改变对应的值,也就是lastIndex=(index, lastIndex)中的最大值,如果满足这个条件,就将旧节点移动到lastIndex的位置;
  • 如果index不存在,新增节点,lastIndex不变;
  • 遍历完新节点后,删除旧节点中多余的节点;

注意:节点比较是通过key值进行比较的,需要保证key的唯一性,使用index做key值的话,会导致每一个节点都找不到对应的key,导致所有的节点都不能复用,都会重新创建。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号