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 其实是:
- 优先按顺序遍历,如果 key 和 type 都一致,就复用
- 遇到不一致,开始用 key 建哈希表(Map)查找可复用节点
- 做节点移动/新增/删除
这是一种近似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 编译阶段做的事
- 把
h1
和span
标记为静态,hoist 出去 - 只把
p
放进动态的 Block - 渲染/更新时,只遍历 Block 内的节点
👉 这就是Block Tree
只对 Block 做 diff,别的不用管。
📌 Block Tree 的好处
- 极大减少 diff 遍历的节点数量
- 保证静态节点不用参与更新
- 越复杂的模板,节省得越多
📊 它们是怎么配合的?
- 静态提升:把不会变的内容提升成常量,跳过 patch
- 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.defineProperty
到 Proxy
」这两个核心变化,其实还有很多很重要的架构、性能、功能级别的更新。我来给你系统、清晰、靠谱地列一份👇:
📌 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 生命周期:
beforeCreate
→setup()
created
→setup()
- 其他生命周期也统一有了 onXxx() 版本,如:
onMounted
onUnmounted
onUpdated
onBeforeMount
✅ 🔟 全新编译器 & Vite 优化
- Vue 3 编译器支持静态提升、patchFlag,更高效
- 天然适配 Vite,冷启动和热更新极快
✅ 🔥 其他小优化
- 更合理的 emits 定义、props 校验
- 自定义指令 API 调整
v-model
可以多个、且双向绑定 prop 名可自定义defineExpose
、defineEmits
、defineProps
新增宏指令- 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 2 | Vue 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
👉 假设新旧节点是oldVnode
和vnode
✅ 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 的
tag
和key
相同,执行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)
✅ 四指针法:
oldStartIdx
、oldEndIdx
newStartIdx
、newEndIdx
✅ 比较顺序:
oldStartVnode
vsnewStartVnode
oldEndVnode
vsnewEndVnode
oldStartVnode
vsnewEndVnode
oldEndVnode
vsnewStartVnode
- 如果能命中,执行
patchVnode
- 如果找不到,执行新增或删除节点操作
👉 这里前提是:
这些子节点是属于同一个根 VNode 的 children 数组
即它们是挂在同一个根节点下的兄弟节点。
📌 为什么必须单根?
👉 Vue 2 的 patch 流程是以根 VNode 为入口,管理其子树
如果没有根节点,Vue 2:
- 没办法确定挂载的 DOM 容器(因为
vnode.elm
只有一个) - 没法维护
parentElm
和refElm
- 子树 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 都可以批量管理一组兄弟节点
- 每组兄弟节点可以视作「虚拟根」,一起插入/删除/更新