如何用JavaScript实现虚拟DOM_diff算法如何减少重绘

发布时间 - 2025-12-31 00:00:00    点击率:
虚拟 DOM 的核心是通过 diff 算法将多次 DOM 变更聚合成一次最小化更新:同层比对、key 驱动、就地复用;仅更新变更属性,不重建节点,从而减少重排重绘。

为什么直接操作 DOM 会导致频繁重绘

浏览器每次调用 document.createElementelement.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(vnodeToMove.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 propschildren
  • JSON.stringify 比较 props 是否变化 —— 开销大且无法处理函数、Symbol、循环引用
  • 未跳过静态节点(如纯文本、无绑定属性的 div)—— 这类节点可标记 static: true,diff 时直接跳过比对
  • patchProps 中对每个 prop 都调用 el.setAttribute —— 应区分 styleclass、事件监听器等,走专用更新路径

真正减少重绘,靠的不是 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接口,云海电视怎样自定义添加电视源?  浏览器如何快速切换搜索引擎_在地址栏使用不同搜索引擎【搜索】  千库网官网入口推荐 千库网设计创意平台入口  如何在万网开始建站?分步指南解析