尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
更新DOM完全指南:从原生操作到虚拟DOM渲染机制
有人问更新DOM是什么操作我认真想了想这个问题有人问“更新DOM是什么操作”的时候往往是遇到了这样一种情况代码里明明把数据改了页面上却纹丝不动。或者更糟页面动了但输入框的焦点丢了、滚动条跳了整个界面肉眼可见地卡了一下。更新DOM听起来就是把页面内容换一换。但真正操作过的人都知道这四个字背后是一条完整的渲染流水线从JavaScript到样式计算、再到布局、绘制、合成任何一环没处理好都会变成卡顿和Bug。这篇文章就来把这套流水线拆开从最基础的DOM操作方式讲起再到浏览器底层渲染机制最后聊聊框架为什么要用虚拟DOM来接管更新这件事。不管你是写原生JavaScript做交互的新手还是刚接触框架、想看明白virtual dom原理的开发者这篇文章都值得花十分钟读完。1. 先理清楚更新DOM到底在更新什么1.1 DOM是浏览器的“内部台账”不是页面本身很多人有个误解觉得DOM就是网页上能看到的东西。严格来说DOM是一份存放在内存里的对象模型是浏览器把HTML文档解析之后生成的一棵节点树。你在页面上看到的文字、图片、按钮实际上是浏览器根据这棵节点树“画”出来的结果。可以这样类比DOM像是建筑公司手里那份详细的施工图纸页面上渲染出来的内容是已经盖好的楼。你更新DOM就相当于修改施工图纸。图纸改了楼当然要重新修一轮但修楼这个动作本身是有成本的而且不是所有修改都会影响整栋楼的结构。修改一份图纸上的某个房间名和修改承重墙的位置施工范围和成本完全不一样。这也就解释了为什么“更新DOM”值得被单独拎出来讨论。它不只是改数据这么简单每一步改动都可能触发浏览器重新计算样式、重新排版、重新绘制这些操作的链路很长成本很高。1.2 “更新”这件事为什么有那么多坑直接在浏览器控制台跑document.getElementById(app).innerHTML hello看起来就更新了。但放到真实项目里问题会一层层冒出来更新之后页面没反应可能是事件监听器被覆盖了。更新频率太高性能面板上全是密密麻麻的渲染任务。更新到一半用户正在输入结果整个列表被重建输入框当场失焦。多个地方同时更新同一个节点顺序错了数据互相覆盖。这些问题背后的根源在于DOM操作是“命令式”的。你得手动精确地告诉浏览器你要改哪个节点、改成什么样、在什么时机改。一步没想清楚后面就会出状况。所以理解更新DOM本质上是在理解“浏览器如何响应你的修改指令”。2. 五种直接更新方式怎么选从innerHTML到textContent2.1 动手前先看清手边的工具原生API提供了好几种直接操作DOM的方式它们看起来都能达到“更新页面”的效果但底层机制完全不同。下面这张表整理了我平时最常用的几种操作方式关键差异都在里面操作方式底层机制是否解析HTML是否重建子节点典型场景innerHTML赋值序列化并解析HTML字符串是会旧节点全部销毁重建整块区域大换血insertAdjacentHTML解析HTML并插入指定位置是仅新增在某元素前后插入一段结构textContent赋值把节点内容当作纯文本替换否子节点会被移除只改一段文字appendChild/insertBefore把已有节点移动到新位置否不影响其他子节点列表追加项replaceChild替换单个子节点否仅替换目标节点局部替换一项注意看最后一列使用场景其实有很强的倾向性。我见过不少同学把innerHTML当万能工具任何更新都先拼个HTML字符串再说这往往是很多性能问题和交互Bug的起点。2.2 选择更新方式的三个判断依据每次动手更新DOM之前我会在脑子里过三个问题第一更新范围是局部还是整体只改列表里某一项的文本那就定位到那个节点用textContent千万别去重建整个列表。改动范围越小浏览器后续需要做的计算就越少。第二数据是纯文本还是结构化内容如果是用户输入的内容任何情况下都要用textContent不要用innerHTML。拿innerHTML插入用户数据等于把XSS攻击的大门敞开这个习惯要从一开始就养成。第三原有的事件监听和表单状态还要不要保留innerHTML会销毁节点内部的一切包括绑定在子元素上的监听器。如果你替换的是一个输入框所在区域的容器用户正在输入的内容和焦点都会瞬间丢失。2.3 一个高频误区随手就用innerHTML我一度也是innerHTML重度用户直到有一次维护一个聊天窗口需要每秒刷新消息列表。最开始图省事直接读数据、拼字符串、塞进容器。结果发现两个问题第一用户在输入框打字消息一刷新输入框就失焦了第二列表项多的时候滚动位置会突然回到顶部。原因很简单——innerHTML是把整个列表的内容全部销毁重新生成输入框被重建焦点自然就没了。后来我改成先对比新旧消息只把新增的消息appendChild进去再把过期消息移除问题立刻消失。这个案例说明了一个核心原则能用局部操作解决问题就不要整块替换。3. 浏览器渲染路径一次DOM更新的完整旅程3.1 从改DOM到屏幕画出来完整链路拆解当你修改了一个DOM节点的文本或者样式浏览器并不会立刻重画屏幕。它走的是下面这条流水线JavaScript你的代码执行DOM节点被修改浏览器标记该节点为“脏”状态。Style Calculation浏览器重新计算这个节点及其子节点的样式。注意不只是被改的那个节点所有受选择器影响的节点都可能被重新计算。Layout计算几何信息。这个节点尺寸多大、位置在哪、有没有挤压到周围元素。这一步在业界也叫reflow重排。Paint把节点绘制成屏幕上的像素比如背景色、文字颜色、边框。这一步叫repaint重绘。Composite把多个图层合成最终画面在GPU上完成。每一步都是有代价的。其中Layout的代价最高因为一个节点的尺寸位置变化可能牵连它的父节点、兄弟节点、后代节点全部重新计算。Paint次之但大面积重绘同样会造成帧率下降。3.2 读写交错强制同步布局为什么会卡Layout本身是异步的浏览器会在合适的时机统一批量计算。但这个优化有一个前提你不在布局信息被读取的时候打破它。举个反面例子const list document.getElementById(list); for (let i 0; i list.children.length; i) { const item list.children[i]; const height item.offsetHeight; // 读取布局信息 item.style.height (height / 2) px; // 写入样式 }这段代码看起来没毛病实际上每次执行offsetHeight之前浏览器都被迫先同步做一次布局计算因为你要读的是“最新的”布局数据。循环里一次读、一次写、再读、再写浏览器就被迫反复执行Layout这就是所谓的“强制同步布局”。用生活场景来类比仓库管理员本来打算下班前统一理一次货结果你每隔五分钟就问他“现在某个箱子在哪个位置”他只能每次放下手里的活先去数一遍箱子。理货本身就慢再被反复打断整体效率自然一塌糊涂。3.3 更新操作的代价往往是由“连锁反应”放大我常见到有人只改了一个节点的背景色整个页面都跟着闪一下。这就是因为修改样式时浏览器无法精确判断这个改动的波及范围它会保守地扩大重绘区域。具体来说改color只会触发重绘改width会触发重排且必然伴随重绘改transform则只触发合成不触发重排和重绘。知道这些区分以后能帮你在设计更新策略时做出更聪明的选择。比如想让元素移动位置用transform: translate()而不是top/left前者完全走GPU合成性能高得不是一个量级。4. 性能优化核心把多次改动合并成一次渲染4.1 浏览器帮你合并但有个前提同一个JavaScript任务里连续改了三次DOM浏览器通常会合并成一次渲染。这意味着定义一个函数在里面做完所有DOM改动优于在循环体外东一榔头西一棒子地改。但前提是改动过程中不穿插读取布局信息的操作。如果你先写一个样式立刻又读offsetWidth浏览器就被迫中止合并先执行一次布局。我踩过最深的坑就是在监听滚动事件的函数里既读scrollTop又去修改其他元素位置结果每次滚动都触发多次强制布局页面直接掉到十几帧。4.2 三个合并技巧抄完就能用第一个技巧是用DocumentFragment离线组装节点const fragment document.createDocumentFragment(); for (let i 0; i 1000; i) { const item document.createElement(div); item.textContent 第 i 项; item.className list-item; fragment.appendChild(item); } // 一次性挂载只触发一次回流 list.appendChild(fragment);DocumentFragment是一个虚拟容器所有节点在它里面组装不会触发任何渲染挂载到真实DOM时只产生一次插入操作。实测生成千级节点时这种方式比在循环中直接appendChild快一个数量级。第二个技巧是批量写入样式时优先切换className而不是逐条修改style属性// 不推荐每改一个样式属性都可能触发一次样式计算 el.style.padding 20px; el.style.fontSize 18px; el.style.color #333; // 推荐用类选择器统一处理浏览器只需要做一次样式计算 el.className card card--expanded;第三条是业务层的技巧本质上是“缓存查询结果”。循环里如果要反复读取某个节点的几何信息提前在循环外算好存进变量不要在每次迭代中重新查询。4.3 requestAnimationFrame把DOM改动调度到正确的时机如果你需要在浏览器下一帧绘制之前做DOM更新requestAnimationFrame是一个可靠的选择。它保证回调执行在样式计算和布局之前所有改动能在同一帧内统一完成避免跨帧的不一致。注意和setTimeout的区别。setTimeout不保证跟显示器刷新率同步它可能被其他任务挤到奇怪的时机执行导致这一帧改了一点下一帧又改了一点看起来就是抖动。我在做动画相关的更新时一律用requestAnimationFrame实测帧率稳定很多。5. 框架的答案虚拟DOM和diff如何接管更新5.1 手动更新DOM的终极难题很难不出错手动更新DOM有两条绕不开的痛点一是逻辑分散数据变化的源头可能分布在整个代码库的各个角落每处都得手动同步视图二是性能边界模糊很容易写出能跑但低效的代码。这就是为什么现代框架要提出“状态驱动视图”的思路。你只管状态框架负责把状态的映射变成界面变化这种方式在工程上减少了大量心智负担。5.2 虚拟DOM加diff先算后做降低更新成本虚拟DOM的思路可以概括为三个步骤第一步状态变化后框架执行渲染函数生成一棵全新的对象树虚拟DOM树。这棵树是在JavaScript内存中构建的开销很小。第二步对比新旧两棵对象树找出差异。这一步叫 diff。diff 的结果是一个“最小变更集合”哪些节点需要新增、删除、修改。第三步根据变更集合定向执行真实DOM操作。这一步叫 patch。真实DOM只被精确地修改一次不会出现大规模的冗余重建。这就像一个记账员手头有一本大账本真实DOM每次数据变化时先在小抄上虚拟DOM快速算清楚要改动哪几行再一次性誊抄到大账本上。誊抄大账本的过程很贵所以尽量减少誊抄的次数和行数。5.3 key 的作用给diff一个“锚点”diff 算法里有一个关键参数key它决定了两个节点在对比时是否被认为是“同一个节点”。为什么列表渲染一定要传key因为没有 key 的时候框架只能靠位置来对应新旧节点。如果列表顺序变了它可能把旧的第三个节点当作新的第三个节点处理从而执行错误的更新。举个例子一个待办列表原来有三项后来第一项被删除了。没有 key 时框架对比规则是“位置相同即复用同一个节点”那么它把旧的第二项当作新的第一项、旧的第三项当作新的第二项去更新每一项的内容都改了白白多做了两次DOM更新。有了稳定唯一的 key 之后框架可以直接识别出新的第三项其实对应旧的第三项只需要删除第一项真正的改动量缩减到最小。从这层意义上看key 本质上是给虚拟DOM节点提供“身份标识”好让 diff 算法做出更精准的判断。这也是为什么框架官方文档反复强调不要用 index 当 key。5.4 框架不等于万能药该手动时还得手动框架把90%的DOM更新自动化了但剩下10%的边界场景你仍然需要手动更新DOM。比如第三方图表库初始化了实例数据变化后需要调用实例的update方法比如要在某个DOM节点上做自定义绘制框架根本不知道这个节点的内部状态。这些时候就得用框架暴露的接口去操作真实DOM比如ref、useRef、watchEffect等。理解这些工具的本质有助于判断“该不该手动更新”。5.5 无框架场景一个轻量级更新函数function renderList(data) { const container document.getElementById(root); const newVNodes data.map(item ({ id: item.id, text: item.text })); // 对比容器当前子节点和期望节点集合 const existingIds [...container.children].map(child child.dataset.id); const removed existingIds.filter(id !newVNodes.some(v v.id id)); removed.forEach(id { container.querySelector([data-id${id}]).remove(); }); newVNodes.forEach(vnode { const el container.querySelector([data-id${vnode.id}]); if (el) { if (el.textContent ! vnode.text) el.textContent vnode.text; } else { const child document.createElement(div); child.dataset.id vnode.id; child.textContent vnode.text; container.appendChild(child); } }); }这段代码的精髓是先算出差集再执行最小操作。没有引入任何框架但遵循的是和虚拟DOM一样的思路。这也是框架带给我们的最有价值的东西——不是某个具体的API而是一种“更新前先算清楚”的思维模式。6. 高频故障与排查思路实录为什么页面没变、卡顿、失焦6.1 页面为什么没更新四个常见原因页面在数据变化之后没有更新是“更新DOM”最常见的一组问题。我在排障时通常会按下面这个顺序逐层排查现象可能原因排查方法数据改了视图没变操作的是旧DOM引用或更新代码未执行在更新语句处打断点确认代码真的走到了视图变了但内容不对异步数据返回时序错乱打印数据更新顺序核对请求并发场景输入框光标丢失容器被innerHTML重建状态被重置检查更新方式局部操作替换全量替换动画抖动、掉帧强制同步布局或频繁触发重排性能面板录制查找紫色布局任务特别说一下异步时序错乱的问题。多个接口同时发起返回顺序不是你能保证的。如果两个请求先后到达但先发出去的请求最后返回它就会覆盖后返回的数据。解决方案是给数据加版本号或者用可取消请求的方式避免过期数据参与渲染。6.2 性能问题定位三板斧如果页面卡顿大概率不是JavaScript计算慢而是渲染链路被拖垮了。打开浏览器的性能面板录制十几秒操作重点看三样东西一是长任务标记。主线程被一段超过50ms的JavaScript占住会导致掉帧此时需要优化数据处理逻辑。二是强制同步布局警告。性能面板会明确标出“Forced reflow”字样提醒你改了样式后立刻读取布局属性。找到对应代码拆开读写。三是Layout Shifts。这个指标看似是视觉稳定性问题但根源往往就是更新DOM时干扰了其他元素的尺寸位置。记住一个委托原则能在CSS层解决的问题永远不要到JavaScript层解决。比如展开收起动画完全可以靠加一个类名让CSS去过渡不需要在脚本里一步一走地改样式。6.3 动手更新前先问自己五个问题我习惯把更新DOM当成一次“施工”施工之前先做评估这次更新改动的是内容还是结构纯文字内容用textContent结构新增用insertAdjacentHTML或appendChild。能不能只更新“变化的那一部分”比较新旧数据只增删改差异项以最小化的方式执行DOM操作。循环里有没有重复查询有就提出去缓存结果。有没有更上层的手段能用className搞定就不要逐条改样式。我是否理解了这次更新会触发的渲染管线阶段如果答案是否先查文档别急着写代码。7. 一个对比实验三种更新策略的实测差距7.1 实验场景和三种方案为了直观展示“更新DOM的打开方式”对性能的影响我写了一个模拟页面一个列表初始渲染500个列表项然后每两秒用新数据刷新一次所有列表项的内容都会变化。方案A整块重构。拿到新数据后直接container.innerHTML 拼接好的全部HTML。方案B按文本节点更新。遍历现有子节点逐个用新的文本内容替换。方案C差分更新。先算出新旧数据的差异只更新变化的项新增的插入删除的移除。用performance.now()记录每次更新的耗时每个方案跑20次取中位数。结果非常直观方案平均耗时ms触发的渲染次数焦点/滚动影响方案AinnerHTML全量约8.5ms1次较大的重建高风险输入框焦点必丢方案B遍历更新文本约1.8ms500次局部更新低风险但高频时掉帧方案C差分更新约0.6ms仅差异节点更新最低风险影响面最小方案A虽然视觉上代码最短但耗时是方案C的十倍以上。原因是它每轮都要序列化整个字符串、解析整个HTML、销毁全部旧节点、创建全部新节点方案B看似更轻但它在循环里逐节点修改触发的局部更新数量太多方案C既减少了节点操作次数又把每次更新的成本压到最低。7.2 方案C具体怎么实现方案C可以用前面写的renderList函数为核心思路。关键在于拿旧数据和新数据做对比生成三个集合新增列表需要appendChild、删除列表需要remove、更新列表需要改textContent。只有更新列表里那些内容真正变化了的项才需要写入真实DOM。这个实验给我的最大启发是性能瓶颈很多时候不是工具的问题而是策略的问题。把更新范围限制到最小比用任何高性能API都更重要。7.3 从实验看更新DOM的优化层级通过这个实验可以总结出一个清晰的优化路径我一般称为“三层递进”第一层减少DOM操作次数能用一次搞定就不用一百次第二层降低每次DOM操作的成本用textContent而不触发HTML解析用class切换代替样式逐条改写第三层对渲染管线进行调优把改动集中到同一帧利用requestAnimationFrame等机制归并渲染任务。这三层不是独立的而是递进的。先控制数量再控制单价最后控制时机所有关于更新DOM的性能问题都能从这三层的角度找到答案。一个收尾的实操建议我个人在实际排错中最大的体会是遇到和更新DOM相关的问题千万不要急着写代码先搞清楚“这一步操作会触发哪一级渲染管线”。把更新DOM看作一次调度任务而不是单纯的改数据很多焦点丢失、页面卡顿、数据不显示的Bug都能从源头避开。最后再分享一个小技巧如果你的项目还在手动拼字符串更新页面立刻停下来先把“所有更新入口”罗列出来统一收敛到一个渲染函数里。哪怕不用框架也能清晰地控制更新边界。这个改造动作的长期收益远大于你第一眼看到它时预估的工程量。
RELATED

相关推荐

基于SpringBoot+Vue的智能图书馆管理系统全栈实战

基于SpringBoot+Vue的智能图书馆管理系统全栈实战

1. 从一次借书排队说起:这个系统解决的实际问题先说清楚我为什么动手做这个系统。基于SpringBoot Vue的智能图书馆管理系统,最早是源自一个困扰了很久的业务场景:某高校图书馆的日常借阅管理,被几套老系统和手工台账拆得稀碎。当…

📅 2026/10/17 11:34:36
缺陷处理流程与软件测试生命周期:一线团队实操指南

缺陷处理流程与软件测试生命周期:一线团队实操指南

做软件测试这些年,被问得最多的问题不是“哪个测试工具好用”,也不是“怎么设计测试用例”,而是更基础也更要命的一个——发现一个Bug之后,到底该怎么处理才不算错?尤其是2020年前后那段时间,很多团队正从传…

📅 2026/10/17 11:34:35
JAVA版WMS物流仓储源码:PDA端与Web端实战解析

JAVA版WMS物流仓储源码:PDA端与Web端实战解析

简介:面向物流仓储企业及Java开发者的一套JAVA版WMS物流仓储管理系统源码,包含PDA端与Web端,适用于自营仓储、第三方物流等场景,可支撑订单、仓储、计费及现场作业全流程。技术架构以SpringMVC、Hibernate、Minidao、EasyUI等为核…

📅 2026/10/17 11:34:35
MORE NEWS

更多资讯

📰

低查重AI教材生成工具实战:从查重机制到编写流程全解析

做教材编写这行久了,谁没被“查重率”三个字折腾过。早年间写教材是纯手工活儿,一个字一个字地抠;后来用AI辅助,效率是上来了,但生成的稿子拿去过查重,经常碰一鼻子灰。今天要聊的,是我最近深度…

📰

AI论文生成工具实用指南:定位、选型与全流程操作

先聊个实际的:打开任何一个论文相关的论坛或者交流群,都能看到有人在问“AI写论文到底行不行”“哪款工具生成的内容能直接交”。我自己用AI辅助写作至少有两年多,经手修改的论文、报告、课程作业加起来几十篇,可以很负责任地说一…

📰

物理AI落地指南:从具身智能到仿真训练,职场人如何抓住机会

物理AI这个词,最近在技术圈里已经被聊烂了,但职场人听到它,往往是一脸茫然:感觉跟自己有点关系,又不知道具体是什么,更不知道该不该着急。我最初接触到这个概念的时候,也是被一堆名词绕得头晕—…

📰

AD域控FSMO角色详解与迁移实战:五大角色职责、转移与抢占

搞IT运维的朋友,尤其是管过域环境的老哥,一定对AD域控里的FSMO角色不陌生。作为Active Directory里五个不能同时由多台域控“商量着来”的特殊角色,它们就像是公司里审批流程必须经手的几把专用公章,谁拿着哪把章、这章现在放哪儿…

📰

React测试框架选型指南:从Jest到Vitest,组件测试的工程化落地

1. 为什么React测试的选型会决定项目体验先说个我自己的经历。去年在某团队接手一个中后台项目,测试代码有三千多个用例,看上去覆盖率挺漂亮,但每次跑全量测试要十五分钟以上,而且随便改动一个公共组件,就会连带挂掉百…

📰

遥感数字图像处理复习题通关指南:从像素到频率域的核心概念与实战

简介:这份南师大遥感数字图像处理复习题doc文档,面向遥感、测绘与地理信息相关专业的本科生及备考学生,用于梳理课程重点、巩固名词概念与查漏补缺。资源包内含1个doc文件,大小约272KB,以文字题目与参考答案为主&#…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬