vue3 和 react 虚拟dom

2 阅读7分钟

Vue 3 和 React 的虚拟 DOM 在核心概念上类似,但在实现细节、优化策略和使用方式上存在显著差异。以下是两者的关键对比:

一、核心实现差异

1. Diff 算法

  • Vue 3

    • 使用 预处理 + 最长递增子序列(LIS 算法,时间复杂度为 O(n log n)
    • 优先处理相同前置 / 后置元素,快速跳过无需比较的节点
    • 通过ShapeFlag位运算快速判断节点类型
  • React

    • 使用双指针遍历 + key 比较,默认时间复杂度为 O(n)

    • 依赖key属性识别同层节点变化

    • 2020 年后引入Fiber 架构,将渲染任务拆分为小单元(可中断)

javascript

// Vue 3的Diff算法(简化)
// 1. 预处理相同前置/后置节点
// 2. 处理新增/删除节点
// 3. 使用LIS算法计算最小移动次数
function patchKeyedChildren(c1, c2, container, parentAnchor) {
  let i = 0;
  const l1 = c1.length;
  const l2 = c2.length;
  let e1 = l1 - 1; // 旧节点的结束索引
  let e2 = l2 - 1; // 新节点的结束索引

  // 1. 预处理相同前置节点
  // (a b) c
  // (a b) d e
  while (i <= e1 && i <= e2) {
    const n1 = c1[i];
    const n2 = c2[i];
    if (isSameVNodeType(n1, n2)) {
      patch(n1, n2, container);
    } else {
      break;
    }
    i++;
  }

  // 2. 预处理相同后置节点
  // a (b c)
  // d e (b c)
  while (i <= e1 && i <= e2) {
    const n1 = c1[e1];
    const n2 = c2[e2];
    if (isSameVNodeType(n1, n2)) {
      patch(n1, n2, container);
    } else {
      break;
    }
    e1--;
    e2--;
  }

  // 3. 处理新增节点(旧节点已遍历完,新节点有剩余)
  // (a b)
  // (a b) c d
  // i = 2, e1 = 1, e2 = 3
  if (i > e1) {
    if (i <= e2) {
      const nextPos = e2 + 1;
      const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor;
      while (i <= e2) {
        patch(null, c2[i], container, anchor);
        i++;
      }
    }
  }

  // 4. 处理删除节点(新节点已遍历完,旧节点有剩余)
  // (a b) c d
  // (a b)
  // i = 2, e1 = 3, e2 = 1
  else if (i > e2) {
    while (i <= e1) {
      unmount(c1[i]);
      i++;
    }
  }

  // 5. 处理乱序节点(核心Diff)
  // a b [c d e] f g
  // a b [e d c h] f g
  else {
    const s1 = i; // 旧节点的开始索引
    const s2 = i; // 新节点的开始索引

    // 5.1 建立新节点的key到index的映射
    const keyToNewIndexMap = new Map();
    for (i = s2; i <= e2; i++) {
      const nextChild = c2[i];
      if (nextChild.key !== null) {
        keyToNewIndexMap.set(nextChild.key, i);
      }
    }

    // 5.2 遍历旧节点,寻找匹配的新节点
    let j;
    let patched = 0;
    const toBePatched = e2 - s2 + 1;
    let moved = false;
    let maxNewIndexSoFar = 0;
    const newIndexToOldIndexMap = new Array(toBePatched).fill(0);

    for (i = s1; i <= e1; i++) {
      const prevChild = c1[i];
      if (patched >= toBePatched) {
        // 所有新节点都已处理,剩余旧节点全部删除
        unmount(prevChild);
        continue;
      }

      let newIndex;
      if (prevChild.key !== null) {
        // 通过key查找新节点位置
        newIndex = keyToNewIndexMap.get(prevChild.key);
      } else {
        // 没有key,遍历查找
        for (j = s2; j <= e2; j++) {
          if (
            newIndexToOldIndexMap[j - s2] === 0 &&
            isSameVNodeType(prevChild, c2[j])
          ) {
            newIndex = j;
            break;
          }
        }
      }

      if (newIndex === undefined) {
        // 没有找到匹配的新节点,删除当前旧节点
        unmount(prevChild);
      } else {
        // 保存旧节点索引(+1 是为了避免与默认值0冲突)
        newIndexToOldIndexMap[newIndex - s2] = i + 1;
        
        // 判断节点是否需要移动
        if (newIndex >= maxNewIndexSoFar) {
          maxNewIndexSoFar = newIndex;
        } else {
          moved = true;
        }
        
        // 复用旧节点,更新内容
        patch(prevChild, c2[newIndex], container);
        patched++;
      }
    }

    // 5.3 使用LIS算法计算最小移动次数
    const increasingNewIndexSequence = moved
      ? getSequence(newIndexToOldIndexMap)
      : [];
    j = increasingNewIndexSequence.length - 1;

    // 5.4 移动和插入节点
    for (i = toBePatched - 1; i >= 0; i--) {
      const nextIndex = s2 + i;
      const nextChild = c2[nextIndex];
      const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;

      if (newIndexToOldIndexMap[i] === 0) {
        // 新节点,需要插入
        patch(null, nextChild, container, anchor);
      } else if (moved) {
        // 需要移动节点
        if (j < 0 || i !== increasingNewIndexSequence[j]) {
          move(nextChild, container, anchor);
        } else {
          j--;
        }
      }
    }
  }
}

// 判断两个VNode是否可以复用(key和type都相同)
function isSameVNodeType(n1, n2) {
  return n1.type === n2.type && n1.key === n2.key;
}

// 最长递增子序列算法(Vue 3源码实现)
function getSequence(arr) {
  const p = arr.slice();
  const result = [0];
  let i, j, u, v, c;
  const len = arr.length;
  for (i = 0; i < len; i++) {
    const arrI = arr[i];
    if (arrI !== 0) {
      j = result[result.length - 1];
      if (arr[j] < arrI) {
        p[i] = j;
        result.push(i);
        continue;
      }
      u = 0;
      v = result.length - 1;
      while (u < v) {
        c = (u + v) >> 1;
        if (arr[result[c]] < arrI) {
          u = c + 1;
        } else {
          v = c;
        }
      }
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1];
        }
        result[u] = i;
      }
    }
  }
  u = result.length;
  v = result[u - 1];
  while (u-- > 0) {
    result[u] = v;
    v = p[v];
  }
  return result;
}
// React的Diff算法(简化)
// 1. 双指针遍历新旧children
// 2. 根据key判断节点是更新、新增还是删除
function reconcileChildren(current, workInProgress) {
  // 当前渲染的fiber节点
  const currentFirstChild = current.child;
  // 工作中的fiber节点(新的虚拟DOM)
  let workInProgressChild = workInProgress.child;
  
  // 1. 双指针初始化
  let oldFiber = currentFirstChild;
  let newIdx = 0;
  let prevNewFiber = null;
  
  // 2. 第一轮遍历:处理相同位置的节点(快速路径)
  while (oldFiber !== null && newIdx < newChildren.length) {
    const newChild = newChildren[newIdx];
    
    // 2.1 通过key和type判断节点是否可以复用
    const sameType = oldFiber.type === newChild.type;
    
    if (!sameType) {
      // 2.2 类型不同,无法复用,标记旧节点为删除
      if (oldFiber) {
        deleteChild(workInProgress, oldFiber);
      }
      break;
    }
    
    // 2.3 可以复用,创建新的fiber节点
    const newFiber = createWorkInProgress(oldFiber, newChild.props);
    
    // 2.4 连接到DOM树
    if (prevNewFiber === null) {
      workInProgressChild = newFiber;
    } else {
      prevNewFiber.sibling = newFiber;
    }
    prevNewFiber = newFiber;
    
    // 2.5 移动指针
    oldFiber = oldFiber.sibling;
    newIdx++;
  }
  
  // 3. 处理新增节点(旧列表已遍历完,新列表还有剩余)
  if (oldFiber === null) {
    while (newIdx < newChildren.length) {
      const newChild = newChildren[newIdx];
      const newFiber = createFiberFromElement(newChild);
      
      if (prevNewFiber === null) {
        workInProgressChild = newFiber;
      } else {
        prevNewFiber.sibling = newFiber;
      }
      prevNewFiber = newFiber;
      newIdx++;
    }
    return;
  }
  
  // 4. 处理删除节点(新列表已遍历完,旧列表还有剩余)
  if (newIdx === newChildren.length) {
    while (oldFiber !== null) {
      deleteChild(workInProgress, oldFiber);
      oldFiber = oldFiber.sibling;
    }
    return;
  }
  
  // 5. 复杂情况:乱序节点处理(使用key进行重排序)
  // 5.1 建立旧节点的key到index的映射
  const existingChildren = mapRemainingChildren(oldFiber);
  
  // 5.2 遍历剩余新节点,寻找最佳匹配
  for (; newIdx < newChildren.length; newIdx++) {
    const newChild = newChildren[newIdx];
    
    // 5.3 通过key查找可复用的旧节点
    const matchedFiber = existingChildren.get(
      newChild.key === null ? newChild.type : newChild.key
    );
    
    if (matchedFiber) {
      // 5.4 复用找到的节点
      const newFiber = useFiber(matchedFiber, newChild.props);
      // 从映射中删除已复用的节点
      existingChildren.delete(
        newChild.key === null ? newChild.type : newChild.key
      );
      
      // 连接到DOM树
      if (prevNewFiber === null) {
        workInProgressChild = newFiber;
      } else {
        prevNewFiber.sibling = newFiber;
      }
      prevNewFiber = newFiber;
    } else {
      // 5.5 没有可复用的节点,创建新节点
      const newFiber = createFiberFromElement(newChild);
      
      if (prevNewFiber === null) {
        workInProgressChild = newFiber;
      } else {
        prevNewFiber.sibling = newFiber;
      }
      prevNewFiber = newFiber;
    }
  }
  
  // 5.6 删除所有未被复用的旧节点
  existingChildren.forEach(child => deleteChild(workInProgress, child));
}

// 辅助函数:建立旧节点的key到fiber的映射
function mapRemainingChildren(currentFirstChild) {
  const existingChildren = new Map();
  let existingChild = currentFirstChild;
  
  while (existingChild !== null) {
    if (existingChild.key !== null) {
      existingChildren.set(existingChild.key, existingChild);
    } else {
      existingChildren.set(existingChild.type, existingChild);
    }
    existingChild = existingChild.sibling;
  }
  
  return existingChildren;
}

2. 渲染优化

  • Vue 3

    • 编译时优化:静态提升(Static Hoisting)、Block Tree
    • 自动标记动态节点,减少 Diff 范围
    • 事件处理函数缓存(cacheHandlers
  • React

    • 运行时优化:依赖React.memouseMemouseCallback等手动优化
    • 需要开发者主动控制组件更新(如shouldComponentUpdate
    • 引入Concurrent Mode(实验性)实现优先级渲染

二、实现策略差异

1. 模板 vs JSX

  • Vue 3

    • 主要使用模板语法.vue文件)

    • 编译时生成优化的渲染函数

    • 示例:

      vue

      <template>
        <div>{{ message }}</div>
      </template>
      
  • React

    • 主要使用JSX(JavaScript 语法扩展)

    • 运行时编译 JSX 为React.createElement调用

    • 示例:

      jsx

      function App() {
        return <div>{message}</div>
      }
      

2. 响应式系统

  • Vue 3

    • 内置Proxy-based 响应式系统

    • 自动追踪依赖,精确触发更新

    • 示例:

      javascript

      import { reactive } from 'vue'
      const state = reactive({ count: 0 })
      // state.count变化时自动触发更新
      
  • React

    • 使用不可变数据状态管理库(如 Redux)

    • 通过setStateuseState显式触发更新

    • 示例:

      javascript

      const [count, setCount] = useState(0)
      // 必须调用setCount才能触发更新
      

三、性能优化差异

1. 静态内容处理

  • Vue 3

    • 编译时识别静态节点并提升(Static Hoisting)

    • 静态节点只创建一次,后续渲染直接复用

    • 示例:

      vue

      <div>
        <h1>Static Title</h1> <!-- 编译时提升 -->
        <p>{{ dynamic }}</p>
      </div>
      
  • React

    • 没有编译时优化,静态节点每次渲染都会重新创建
    • 需手动使用React.memo或提取组件避免重复渲染

2. 事件处理优化

  • Vue 3

    • 自动缓存事件处理函数(cacheHandlers

    • 示例:

      vue

      <button @click="handleClick">Click</button>
      <!-- 编译后自动缓存handleClick -->
      
  • React

    • 需要手动使用useCallback缓存回调函数

    • 示例:

      jsx

      const handleClick = useCallback(() => {
        // 处理逻辑
      }, [dependencies])
      

四、架构设计差异

1. 组件更新粒度

  • Vue 3

    • 组件级更新:单个组件状态变化只会触发该组件重新渲染
    • 基于响应式系统精确追踪依赖
  • React

    • 函数式组件默认全量重新渲染
    • 需要通过React.memouseMemo等手动控制

2. 异步渲染

  • Vue 3

    • 渲染过程是同步的,但更新是异步批量
    • 通过nextTick访问更新后的 DOM
  • React

    • Concurrent Mode(实验性)支持异步渲染
    • 可中断渲染过程,优先处理高优先级任务

五、总结对比表

特性Vue 3React
Diff 算法预处理 + LIS(O (n log n))双指针 + key(O (n))
优化方式编译时自动优化(静态提升、Block)运行时手动优化(React.memo)
响应式系统内置 Proxy-based 响应式不可变数据 + 显式 setState
模板语法声明式模板JSX
更新粒度组件级函数式组件默认全量更新
异步渲染异步批量更新Concurrent Mode(实验性)

六、适用场景

  • Vue 3

    • 适合需要高性能渲染的大型应用
    • 对开发效率有较高要求(编译时优化减少手动工作)
    • 偏好声明式模板语法的开发者
  • React

    • 适合需要灵活控制渲染过程的应用
    • 团队熟悉 JavaScript / 函数式编程
    • 需要与复杂状态管理库集成的场景