虚拟 DOM 和 diff 算法
Vue 采用了 虚拟 DOM 和 diff 算法 来大幅提高渲染性能,这是 Vue 框架性能优化的核心机制。
虚拟 DOM
虚拟 DOM 是用 JavaScript 对象模拟 DOM 树结构(包含节点类型、属性和子节点等信息)的轻量级表示。
工作原理
数据变化 → 创建新 VNode 树 → diff 算法对比 → 最小化 DOM 更新
关键特性
- 在 Vue 中称为 VNode,是描述 DOM 节点的 JavaScript 对象
- 数据变化时,Vue 创建新的 VNode 树
- 通过 diff 算法对比新旧 VNode 树,只更新变化的部分到真实 DOM
- 避免了对整个 DOM 树的重绘,显著提升性能
优缺点分析
优点:
- 跨平台兼容(可渲染到原生、WebGL 等)
- 性能优化(批量更新、最小化 DOM 操作)
- 便于测试和调试
缺点:
- 初始化有额外开销
- 简单场景性能不如直接操作 DOM
- 增加了代码复杂度
diff 算法
diff 算法 用于找出两个树形结构之间的差异,是虚拟 DOM 高效更新的关键。
Vue2 双端比对算法
核心流程:
- 比较新旧节点的首尾四个节点
- 若匹配则移动或更新,指针向中间收缩
- 若不匹配则查找,找到后移动节点
- 最后处理剩余节点(新增或删除)
Vue3 最长递增子序列算法
优化原理:
- 找出不需要移动的节点序列(最长递增子序列)
- 只移动剩余需要调整位置的节点
- 大幅减少 DOM 移动操作
key 的关键作用
- 唯一标识:确保节点身份的稳定性
- 精准匹配:快速定位相同节点,避免不必要的创建/销毁
- 避免副作用:防止就地复用导致的状态错误
key 的最佳实践
为什么不建议用数组下标做列表的 key?
核心风险:使用数组下标作为 key 会破坏节点的稳定性,导致错误的 DOM 更新和性能问题。
具体问题
- 状态丢失:输入框内容、组件状态等
- 性能下降:不必要的组件重新渲染
- bug 隐患:列表项状态混乱
最佳实践
- 使用与元素内容强相关的稳定唯一标识(如数据库 id)
- 确保 key 在列表生命周期内保持不变
- 只有当列表固定不变且无状态时,才考虑使用下标
Vue3 diff 算法 vs Vue2 双端比对
Vue3 采用了全新的 diff 算法,相比 Vue2 的双端比对有显著性能提升:
1. 最长递增子序列算法
- Vue3 使用最长递增子序列算法优化节点移动
- 减少不必要的 DOM 移动操作,提升更新效率
2. 静态标记
- 编译器对静态节点进行标记
- 更新时直接跳过静态节点,减少 diff 比对次数
3. 缓存优化
- 缓存新旧 VNode 数组
- 只比对数组中实际变化的 VNode,减少计算量
4. 异步删除操作
- 动态删除操作采用异步队列合并
- 减少 DOM 重排次数,提升渲染性能
对比总结:
| 特性 | Vue2 | Vue3 |
|---|---|---|
| 算法核心 | 双端比对 | 最长递增子序列 |
| 静态节点处理 | 无特殊处理 | 静态标记(直接跳过) |
| 缓存机制 | 无 | VNode 数组缓存 |
| 删除操作 | 同步执行 | 异步队列合并 |
| 性能提升 | - | 约 1.3-2 倍 |
nextTick 的实现原理
nextTick 是 Vue 异步更新机制的核心 API,用于在 DOM 更新完成后执行回调。
核心原理
- 基于浏览器的异步任务队列,采用微任务优先策略
- 数据修改时,DOM 更新操作被放入异步队列
- nextTick 将回调推入同一队列,确保在 DOM 更新完成后执行
Vue2 vs Vue3 实现对比
| 特性 | Vue2 | Vue3 |
|---|---|---|
| 兼容性 | 支持 IE9+,多层降级 | 放弃 IE,仅支持现代浏览器 |
| 实现方式 | Promise → MutationObserver → setImmediate → setTimeout | 仅使用 Promise |
| 复杂度 | 复杂(多层降级) | 简单(单一实现) |
| 更新队列 | Watcher + nextTick 混合 | 独立 Job 队列 + nextTick 回调 |
| 性能 | 降级逻辑有额外开销 | 更纯粹,性能更佳 |
应用场景
- 获取更新后的 DOM:如获取元素尺寸、位置
- 解决时序问题:数据更新后立即操作 DOM
- 优化性能:批量处理 DOM 相关操作
实现细节
Vue2 降级策略逻辑:
// 优先使用 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => { p.then(flushCallbacks) }
}
// 降级到 MutationObserver
else if (
typeof MutationObserver !== 'undefined' &&
(isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
// ... MutationObserver 实现
}
// 降级到 setImmediate
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => { setImmediate(flushCallbacks) }
}
// 最终降级到 setTimeout
else {
timerFunc = () => { setTimeout(flushCallbacks, 0) }
}
核心要点总结
虚拟 DOM & diff 算法
- 虚拟 DOM:JS 对象模拟 DOM,最小化更新,跨平台兼容
- Vue2 diff:双端比对算法,通过首尾指针收缩找到匹配节点
- Vue3 diff:最长递增子序列算法,减少节点移动操作
key 的使用原则
- 必须唯一且稳定,与元素内容强相关
- 禁止在动态列表中使用数组下标
- 推荐使用数据库 id 作为 key 值
Vue3 性能优化
- 最长递增子序列算法优化节点移动
- 静态标记跳过无变化的静态节点
- VNode 数组缓存减少比对计算量
- 异步删除操作合并减少 DOM 重排
nextTick 关键要点
- 基于浏览器异步任务队列,微任务优先执行
- 确保 DOM 更新完成后执行回调函数
- Vue2 采用多层降级策略确保兼容性
- Vue3 仅使用 Promise 实现,更简洁高效
学习提示
理解虚拟 DOM
- 核心是最小化真实 DOM 操作
- 权衡初始化开销与更新性能
- 适合复杂应用场景,简单场景可能性能更差
掌握 diff 算法思想
- Vue2 双端比对的四指针策略
- Vue3 最长递增子序列的优化原理
- key 在节点复用中的关键作用
正确使用 key
- 动态列表必须使用稳定唯一标识
- 避免使用会随列表变化的属性作为 key
- 理解就地复用的潜在风险
深入 nextTick
- 微任务与宏任务的执行顺序
- Vue 异步更新队列的工作机制
- 实际应用中解决 DOM 时序问题