dom树在模板热更新时“丢状态”的根本原因是新旧节点无引用关联,innerhtml或replacechildren()销毁旧节点导致事件、光标、滚动等全部清零;react/vue hmr靠框架协调绕过真实dom替换,而纯html模板无此机制。

DOM树在模板热更新时为何会“丢状态”
根本原因不是浏览器重绘,而是新模板生成的DOM节点与旧节点无引用关联——innerHTML 或 replaceChildren() 会销毁旧节点,所有绑定事件、input 的光标位置、scrollTop、checked 状态全部清零。React/Vue 的 HMR 能保留状态,靠的是框架层绕过真实 DOM 替换,而纯 HTML 模板(如 EJS、Handlebars、或手写 template.innerHTML = ...)没有这层协调机制。
用 DocumentFragment 做增量更新仍会丢状态吗
会,但比直接覆写 innerHTML 少丢一部分。DocumentFragment 本身不触发重排,但它插入后仍是全新节点——input 元素的 value 和 selectionStart 不会自动继承,textarea 的滚动位置、contenteditable 的光标都归零。关键点在于:DocumentFragment 解决的是性能问题,不是状态映射问题。
- 它不保留任何用户交互态,只保证“结构一次性插入”
- 若旧 DOM 中有
addEventListener绑定,新 Fragment 里的同名元素不会继承这些监听器 -
querySelector在 Fragment 上不可用,无法提前捞出旧节点做状态迁移
手动保状态的三个必须动作
没有框架兜底时,想让模板热更新“看起来没刷新”,得自己接管状态迁移。核心是:在替换前读、替换后写、且只动必要字段。
- 对每个要保留状态的元素,提前缓存:
input.value、textarea.scrollTop、select.selectedIndex、element.dataset(非 DOM 属性)、getSelection().toString()(仅 contenteditable) - 用
replaceChildren()或innerHTML更新后,**按 selector 重新 query 出新节点**,再把缓存值赋回去;不能依赖旧引用,因为节点已销毁 - 避免对整个容器粗暴操作——比如给
ul换innerHTML,却想保留某个li内input的焦点;应只更新变动的li子集,用insertAdjacentElement或replaceWith
DOM 复用池能解决热更新状态保持吗
不能直接解决,但可降低状态丢失频率。DOM 复用池(如 domPoolFactory)本质是预创建节点并回收,它要求每次 recover() 前清空 textContent 和 innerHTML ——这恰恰会抹掉用户输入。除非你在回收前主动提取并暂存状态,否则复用等于重置。
真正起作用的环节是:你控制节点生命周期。例如,为每个表单项分配唯一 key,回收时把它的 value 存进 Map,create 时从 Map 注入。但这已超出复用池职责,属于业务状态管理逻辑。
最易被忽略的一点:form 元素的 reset() 行为、autocomplete="off" 对输入历史的干扰、以及 input 的 type="number" 在值非法时自动清空——这些都会在热更新后意外触发,和 DOM 替换本身无关,但效果一样:状态没了。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











