Vue2 vs Vue3 vs React

2 阅读10分钟

Q: react的diff比较为什么不是双指针比较法?

React 的 diff 算法(主要是 Reconciliation 过程)并不是简单的双指针比较,原因是它要兼顾性能和灵活性,具体原因如下:

为什么不用双指针?

双指针法适合两个有序且严格对应的序列,例如:

[1, 2, 3]
[1, 2, 3, 4]

用双指针可以一边遍历一边比较。

但在 React 真实的 DOM 节点更新场景里:

  • 节点可以新增、删除、移动、替换
  • key 值决定了节点身份,不是位置
  • 节点顺序可能会变
    比如:
<ul>
  <li key="a">A</li>
  <li key="b">B</li>
</ul>

变成:

<ul>
  <li key="b">B</li>
  <li key="a">A</li>
</ul>

如果用双指针:

  • 比较到第一位,发现a != b,但它们其实只是换了个位置,直接替换就浪费了。

React 的做法:

  • 先用key做快速映射,建立 old 和 new 的节点映射关系
  • 再判断是否可以复用,是否需要移动,还是要新增/删除
    (比如 React Fiber 中的 ChildReconciler

React 的优化点

React 其实是:

  1. 优先按顺序遍历,如果 key 和 type 都一致,就复用
  2. 遇到不一致,开始用 key 建哈希表(Map)查找可复用节点
  3. 做节点移动/新增/删除

这是一种近似O(n)的策略,比直接双指针和全量对比都更高效。

总结

不是不用双指针,是因为:

  • 节点不一定顺序固定
  • React 要用 key 标识节点身份
  • key 决定了是不是“同一个”节点,位置变化也能复用
    所以它用了一种“顺序+key映射”的混合方案。

Q: react的diff算法和 vue的diff算法有什么不同

这正是 Vue 3 性能提升的核心点之一,我来给你详细讲讲 👉

🌿 Vue 3 的 静态提升 + Block Tree 是什么?

📌 静态提升(Static Hoisting)

简单说: 👉 把模板里不会变的静态内容提取出来,只生成一次,后续 patch 阶段不用再 diff 比较

📌 举个例子:

<template>
  <div>
    <h1>标题</h1>
    <p>{{ message }}</p>
  </div>
</template>

编译结果:

  • <h1>标题</h1> 是静态的,永远不变,Vue 3 编译时就把它提升成常量 hoisted 节点。
  • 只有 <p>{{ message }}</p> 是动态的,需要 diff。

👉 这样:

  • 初次渲染:静态部分和动态部分一起渲染
  • 更新时:只对动态部分(<p>)做 diff,静态部分完全跳过

⚡️少了不必要的 diff,自然更快

📌 Block Tree(区块树)

React 和 Vue 2 以前的 diff,都是整棵 Virtual DOM 树一层层 diff。

Vue 3 改成了: 👉 把需要动态 diff 的节点,分块收集成 Block Tree,其他的静态节点不放进去

📌 举个例子:

<template>
  <div>
    <h1>静态标题</h1>
    <p>{{ message }}</p>
    <span>静态副标题</span>
  </div>
</template>

Vue 3 编译阶段做的事

  • h1span 标记为静态,hoist 出去
  • 只把 p 放进动态的 Block
  • 渲染/更新时,只遍历 Block 内的节点

👉 这就是Block Tree
只对 Block 做 diff,别的不用管。

📌 Block Tree 的好处

  • 极大减少 diff 遍历的节点数量
  • 保证静态节点不用参与更新
  • 越复杂的模板,节省得越多

📊 它们是怎么配合的?

  1. 静态提升:把不会变的内容提升成常量,跳过 patch
  2. Block Tree:只把有可能变的部分组织起来,更新时只 diff 这些

像是:

  • 静态提升 = 把"不动的家具"打包贴好标签
  • Block Tree = 只记录"可能要挪动的东西",其他房间不用看

📈 性能收益?

👉 官方 benchmark 里,Vue 3 的 patch 性能比 Vue 2 提升 1.3~2 倍
尤其是复杂组件、大模板场景,优势越明显。

✅ 总结一句:

静态提升 + Block Tree = 只做必要的最小量 diff,跳过不可能变的部分,渲染超快

Q: vue2到vue3,除了语法上变成了函数式,响应式的底层原理从Object.defineProperty变成了proxy以外,还有什么更新?

Vue 3 除了你说的「语法函数式」和「响应式从 Object.definePropertyProxy」这两个核心变化,其实还有很多很重要的架构、性能、功能级别的更新。我来给你系统、清晰、靠谱地列一份👇:

📌 Vue 2 → Vue 3 核心变化总结

✅ 1️⃣ 响应式系统重构(你说的)

  • Vue 2:基于 Object.defineProperty
  • Vue 3:基于 Proxy
    • 可以监听新增、删除属性
    • 可以监听数组索引和 length
    • 性能更好,死角更少

✅ 2️⃣ Composition API(组合式 API)

除了 data / methods / computed / watch 这些选项式写法
Vue 3 新增了 setup() 和组合式 API

  • ref()
  • reactive()
  • computed()
  • watch()
  • watchEffect()
  • onMounted()onUnmounted() 等生命周期钩子函数化

✔️ 优点:

  • 更好代码逻辑拆分 & 复用
  • 更灵活、类型推导更好
  • 更适合 TypeScript

✅ 3️⃣ 虚拟 DOM 重构(更快更小)

Vue 3 完全重写了 Virtual DOM diff 算法

  • 静态提升(静态节点只创建一次,复用)
  • Patch flag 标记(VNode 节点变化类型更明确,跳过无关 diff)
  • 更快的子树比对和节点复用
  • 更小体积、更快渲染

✅ 4️⃣ Tree-shaking 支持

Vue 3 是模块化开发的,天然支持 Tree-shaking
👉 没用到的 API、功能、指令、模块,打包时直接裁掉
✔️ 优点:

  • 最小化包体积
  • 更适合现代构建工具 (Vite、Rollup、Webpack 5)

✅ 5️⃣ Fragment(多个根节点)

Vue 2:组件必须有一个根节点
Vue 3:支持返回多个根节点,不需要多余的 <div>

<template>
  <h1>标题</h1>
  <p>内容</p>
</template>

✅ 6️⃣ Teleport(传送门)

新组件 <teleport>,可以把某个子内容挂载到 DOM 的任意位置
✔️ 用法:

  • 弹窗、抽屉、全局遮罩
  • 不必受组件结构层级限制

✅ 7️⃣ Suspense(异步组件等待)

Vue 3 新增 <suspense>,可以优雅地处理异步组件加载

  • 异步加载未完成时显示 loading
  • 加载完成后自动替换成内容

✅ 8️⃣ 更好的 TypeScript 支持

Vue 3 完全用 TypeScript 重构
✔️ 优点:

  • 更好的类型推导
  • 更安全的 API 使用
  • 更完善的类型声明

✅ 9️⃣ 生命周期改名 & 统一化

选项式 API 生命周期:

  • beforeCreatesetup()
  • createdsetup()
  • 其他生命周期也统一有了 onXxx() 版本,如:
    • onMounted
    • onUnmounted
    • onUpdated
    • onBeforeMount

✅ 🔟 全新编译器 & Vite 优化

  • Vue 3 编译器支持静态提升、patchFlag,更高效
  • 天然适配 Vite,冷启动和热更新极快

✅ 🔥 其他小优化

  • 更合理的 emits 定义、props 校验
  • 自定义指令 API 调整
  • v-model 可以多个、且双向绑定 prop 名可自定义
  • defineExposedefineEmitsdefineProps 新增宏指令
  • slots 写法简化

📌 总结一句话:

Vue 3 是一次彻底底层重构+现代化升级: ✅ 响应式更强
✅ 组合式 API 更自由
✅ 架构更快更轻
✅ TypeScript 更香
✅ 更适合现代前端生态(Vite、ESM、Tree-shaking)

Q: 为什么vue2不支持template下有多个节点,但是vue3支持fragment下多个根节点?

我们来从底层机制和设计思想上讲清楚这个问题。

📌 为什么 Vue 2 不支持 template 下多个根节点?

✅ Vue 2 虚拟 DOM 的限制

在 Vue 2 中:

  • 每个组件的 template → 会被编译成一个 VNode
  • 这个 VNode必须有唯一的根节点

原因: 👉 Vue 2 的 Virtual DOM diff 算法 是基于「单一根节点」假设的
它会在组件 mount 和 update 时,假定:

  • 你这个组件渲染的内容就是挂载在某个容器里的一个根 VNode
  • 这个根 VNode 内部可以有子节点,但必须是挂在一个节点下面的

如果有多个根,Vue 2 的 patch 流程就不知道该怎么管理:

  • 插入、删除、替换哪个是组件的“根”?
  • 组件的生命周期挂载点、DOM 插入点、销毁点,全都混乱了

所以 Vue 2 强制:

<template>
  <div>必须有唯一的根节点</div>
</template>

📌 为什么 Vue 3 支持 fragment(多个根节点)?

✅ Vue 3 重写了 Virtual DOM 和 diff 算法

Vue 3 的 Virtual DOM:

  • 重构了 VNode 数据结构
  • 支持了一个特殊类型 Fragment 节点
    👉 Fragment 是一个虚拟的、无实体的容器,表示「一组兄弟节点」

举个例子:

<template>
  <h1>标题</h1>
  <p>内容</p>
</template>

Vue 3 编译时,会把它转换成:

createVNode(Fragment, null, [
  createVNode('h1', null, '标题'),
  createVNode('p', null, '内容')
])

Vue 3 diff 算法:

  • 知道 Fragment 只是个“壳”,会直接渲染/更新/卸载它里面的子节点
  • 生命周期、事件绑定、DOM 插入顺序都能正确管理

✅ 所以:Vue 3 不需要根节点限制了,Fragment 就是“虚拟的根

📌 总结一句话:

Vue 2Vue 3
VNode 必须有唯一根节点VNode 支持 Fragment,允许一组兄弟节点
diff 和 patch 基于单根节点假设diff 和 patch 支持 Fragment 批量挂载
模板层必须包装一个根元素模板层可直接写多个平级节点

📌 延伸 👉 为什么 React 也有 Fragment?

因为它和 Vue 3 类似,也是为了虚拟 DOM 和 diff 算法解耦单根节点限制

<>
  <h1>标题</h1>
  <p>内容</p>
</>

底层一样是:

React.createElement(React.Fragment, null, [...])

Q: vue2对于单根节点的patch这里的算法给我展示和解释一下

我们来清晰还原一下 Vue 2 中 patch 的单根节点机制和核心算法思想,我来带你从VNode 数据结构、patch 流程、diff 算法逐步讲清楚。

📌 Vue 2 的 VNode 数据结构(单根节点)

先看一个 Vue 2 中组件渲染的 VNode 结构👇:

{
  tag: 'div',               // 标签名
  data: { attrs: { id: 'app' } }, // 标签属性
  children: [               // 子节点数组
    { tag: 'h1', text: 'Hello' },
    { tag: 'p', text: 'World' }
  ],
  elm: undefined,           // 真实 DOM 节点,patch 阶段赋值
  parent: undefined,        // 父 VNode
}

✅ Vue 2 的前提:

  • 一个组件对应一个根 VNode
  • 这个 VNode 的 children 可以有多个,但它本身是单一的挂载入口

📌 Vue 2 的 patch 流程(简化版)

入口在 src/core/vdom/patch.js
👉 假设新旧节点是 oldVnodevnode

✅ patch 的核心流程:

function patch (oldVnode, vnode) {
  // 1. 判断 oldVnode 是真实 DOM 还是 VNode
  if (!oldVnode.tag) {
    // 初次渲染,挂载到真实 DOM
    const elm = createElm(vnode)
    // 插入到父节点
    insert(parentElm, elm, refElm)
  } else {
    // 2. diff 新旧 VNode
    if (sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode)
    } else {
      // 不同,直接替换
      const parentElm = oldVnode.elm.parentNode
      const elm = createElm(vnode)
      insert(parentElm, elm, oldVnode.elm)
      removeVnodes(parentElm, [oldVnode], 0, 0)
    }
  }
  return vnode.elm
}

📌 Vue 2 的 patchVnode 核心逻辑

✅ 只针对单根的「根 VNode」

  • 如果新旧 VNode 的 tagkey 相同,执行 patchVnode
  • 否则替换整棵子树
function patchVnode (oldVnode, vnode) {
  const elm = vnode.elm = oldVnode.elm

  if (oldVnode === vnode) return

  if (vnode.text) {
    // 文本节点更新
    if (oldVnode.text !== vnode.text) {
      elm.textContent = vnode.text
    }
  } else {
    // 递归对子节点 diff
    updateChildren(elm, oldVnode.children, vnode.children)
  }
}

📌 Vue 2 的 updateChildren Diff 算法(核心)

Vue 2 使用的是双端对比算法(simple O(n) diff)

✅ 四指针法:

  • oldStartIdxoldEndIdx
  • newStartIdxnewEndIdx

✅ 比较顺序:

  1. oldStartVnode vs newStartVnode
  2. oldEndVnode vs newEndVnode
  3. oldStartVnode vs newEndVnode
  4. oldEndVnode vs newStartVnode
  • 如果能命中,执行 patchVnode
  • 如果找不到,执行新增或删除节点操作

👉 这里前提是:
这些子节点是属于同一个根 VNode 的 children 数组
即它们是挂在同一个根节点下的兄弟节点。

📌 为什么必须单根?

👉 Vue 2 的 patch 流程是以根 VNode 为入口,管理其子树
如果没有根节点,Vue 2:

  • 没办法确定挂载的 DOM 容器(因为 vnode.elm 只有一个)
  • 没法维护 parentElmrefElm
  • 子树 diff 也会乱掉(多棵子树,没有统一父 DOM)

举个极端例子:

假如 Vue 2 允许:

<template>
  <h1>标题</h1>
  <p>内容</p>
</template>

生成两个 VNode:

[
  { tag: 'h1', text: '标题' },
  { tag: 'p', text: '内容' }
]

它的 patch 没有共同 parentElm,无法正确:

  • 插入
  • 删除
  • 替换
  • 复用

而 Vue 3 用 Fragment 包裹就解决了这事。

📌 总结一句

Vue 2:

  • patch 基于单根节点假设
  • 组件只能有一个根 VNode
  • 子节点 diff 依附于根 VNode 的 children
  • 没有 Fragment,无法直接管理多棵子树

Vue 3:

  • 引入 Fragment
  • patch 和 diff 都可以批量管理一组兄弟节点
  • 每组兄弟节点可以视作「虚拟根」,一起插入/删除/更新