如何用JavaScript实现虚拟DOM_diff算法如何减少重绘
发布时间 - 2025-12-31 00:00:00 点击率:次虚拟 DOM 的核心是通过 diff 算法将多次 DOM 变更聚合成一次最小化更新:同层比对、key 驱动、就地复用;仅更新变更属性,不重建节点,从而减少重排重绘。
为什么直接操作 DOM 会导致频繁重绘
浏览器每次调用 document.createElement、element.appendChild 或修改 element.innerHTML,都可能触发样式计算、布局(reflow)和绘制(repaint)。尤其是深层嵌套节点变动时,重排会波及父级甚至整个文档流。虚拟 DOM 的核心不是“避免重绘”,而是把多次 DOM 变更聚合成一次最小化更新 —— 关键在 diff 阶段精准识别哪些节点该复用、哪些要新增/删除/移动。
diff 算法必须做的三件事:同层比对、key 驱动、就地复用
React 和 Vue 的 diff 都基于「双端对比 + key 标识」策略,不跨层级比较,也不递归遍历整棵树。重点在于:
-
key必须稳定唯一,不能用index当 key(列表顺序变化时会错乱复用) - 只比对同一层级的 vnode,父节点不同就直接卸载整棵子树
- 元素类型(
tag)或组件类型(type)不同时,不尝试 patch,直接 replace - 属性更新走
patchProps,仅更新变更字段,不全量 setAttribute
例如两个 div 节点,仅 class 不同,diff 后只执行 el.className = 'new',而非重建节点。
手写简易 diff 的关键逻辑(仅比对同层子节点)
以下是最小可行的双指针 diff 示例,聚焦子节点列表更新逻辑:
function diffChildren(oldCh, newCh, parentEl) {
let oldStartIdx = 0, newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let newEndIdx = newCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (!oldStartVnode) {
oldStartVnode = oldCh[++oldStartIdx];
} else if (!oldEndVnode) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode);
parentEl.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode);
parentEl.insertBefore(oldEndVnode.el, oldStartVnode.el);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// fallback:用 key 建哈希表查找可复用节点
const idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
if (idxInOld > 0) {
const vnodeToMove = oldCh[idxInOld];
patchVnode(vnodeToMove, newStartVnode);
parentEl.insertBefore(vno
deToMove.el, oldStartVnode.el);
oldCh[idxInOld] = undefined;
} else {
parentEl.insertBefore(createElement(newStartVnode), oldStartVnode.el);
}
newStartVnode = newCh[++newStartIdx];
}
}
// 清理剩余旧节点
if (oldStartIdx <= oldEndIdx) {
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if (oldCh[i]) removeElement(oldCh[i].el);
}
}
// 插入剩余新节点
if (newStartIdx <= newEndIdx) {
for (let i = newStartIdx; i <= newEndIdx; i++) {
const before = newCh[i + 1] ? newCh[i + 1].el : null;
parentEl.insertBefore(createElement(newCh[i]), before);
}
}
}
容易被忽略的性能陷阱
很多实现卡在看似合理但实际低效的细节上:
- 每次 diff 都深克隆 vnode —— 应复用原始对象,只在必要时 shallow clone
props或children - 用
JSON.stringify比较 props 是否变化 —— 开销大且无法处理函数、Symbol、循环引用 - 未跳过静态节点(如纯文本、无绑定属性的 div)—— 这类节点可标记
static: true,diff 时直接跳过比对 - 在
patchProps中对每个 prop 都调用el.setAttribute—— 应区分style、class、事件监听器等,走专用更新路径
真正减少重绘,靠的不是 diff 多快,而是它能否让 patch 阶段只触达真实需要变更的 DOM 属性和位置。算法再精妙,如果 patch 时仍粗暴 innerHTML 或强制 reflow,优化就白做了。
# vue
# react
# javascript
# java
# html
# js
# json
# node
# 浏览器
# app
# ai
# 重绘
相关栏目:
【
网站优化151355 】
【
网络推广146373 】
【
网络技术251813 】
【
AI营销90571 】
相关推荐:
laravel怎么在请求结束后执行任务(Terminable Middleware)_laravel Terminable Middleware请求结束任务执行方法
长沙做网站要多少钱,长沙国安网络怎么样?
Laravel模型关联查询教程_Laravel Eloquent一对多关联写法
Laravel如何配置Horizon来管理队列?(安装和使用)
Laravel如何使用Collections进行数据处理?(实用方法示例)
东莞市网站制作公司有哪些,东莞找工作用什么网站好?
ChatGPT怎么生成Excel公式_ChatGPT公式生成方法【指南】
Laravel Eloquent:优雅地将关联模型字段扁平化到主模型中
如何在Windows虚拟主机上快速搭建网站?
JavaScript如何实现错误处理_try...catch如何捕获异常?
Laravel如何实现用户密码重置功能?(完整流程代码)
宙斯浏览器怎么屏蔽图片浏览 节省手机流量使用设置方法
Laravel中的withCount方法怎么高效统计关联模型数量
网站制作报价单模板图片,小松挖机官方网站报价?
如何快速打造个性化非模板自助建站?
详解CentOS6.5 安装 MySQL5.1.71的方法
深入理解Android中的xmlns:tools属性
如何获取PHP WAP自助建站系统源码?
移动端脚本框架Hammer.js
Laravel如何理解并使用服务容器(Service Container)_Laravel依赖注入与容器绑定说明
PHP正则匹配日期和时间(时间戳转换)的实例代码
如何获取上海专业网站定制建站电话?
制作企业网站建设方案,怎样建设一个公司网站?
网站优化排名时,需要考虑哪些问题呢?
深圳网站制作公司好吗,在深圳找工作哪个网站最好啊?
如何快速查询网址的建站时间与历史轨迹?
如何在 Python 中将列表项按字母顺序编号(a.、b.、c. …)
如何在香港免费服务器上快速搭建网站?
夸克浏览器网页跳转延迟怎么办 夸克浏览器跳转优化
php静态变量怎么调试_php静态变量作用域调试技巧【解答】
Win11怎么关闭专注助手 Win11关闭免打扰模式设置【操作】
极客网站有哪些,DoNews、36氪、爱范儿、虎嗅、雷锋网、极客公园这些互联网媒体网站有什么差异?
jQuery 常见小例汇总
如何注册花生壳免费域名并搭建个人网站?
Laravel如何使用Seeder填充数据_Laravel模型工厂Factory批量生成测试数据【方法】
ChatGPT回答中断怎么办 引导AI继续输出完整内容的方法
Laravel如何实现邮件验证激活账户_Laravel内置MustVerifyEmail接口配置【步骤】
Laravel怎么实现搜索功能_Laravel使用Eloquent实现模糊查询与多条件搜索【实例】
如何在IIS中新建站点并解决端口绑定冲突?
如何在Tomcat中配置并部署网站项目?
Laravel怎么实现API接口鉴权_Laravel Sanctum令牌生成与请求验证【教程】
Laravel如何配置和使用缓存?(Redis代码示例)
香港服务器WordPress建站指南:SEO优化与高效部署策略
Laravel怎么生成URL_Laravel路由命名与URL生成函数详解
电商网站制作多少钱一个,电子商务公司的网站制作费用计入什么科目?
Laravel Session怎么存储_Laravel Session驱动配置详解
电视网站制作tvbox接口,云海电视怎样自定义添加电视源?
浏览器如何快速切换搜索引擎_在地址栏使用不同搜索引擎【搜索】
千库网官网入口推荐 千库网设计创意平台入口
如何在万网开始建站?分步指南解析


deToMove.el, oldStartVnode.el);
oldCh[idxInOld] = undefined;
} else {
parentEl.insertBefore(createElement(newStartVnode), oldStartVnode.el);
}
newStartVnode = newCh[++newStartIdx];
}
}