04元素增删

DOM 是活的——上一课我们只是「读」页面(找节点、读属性、看内容), 这一课开始「写」页面:createElement 凭空造元素、 append / insertBefore 插到任意位置、 remove / replaceWith 删掉或替换任何东西。 把这四件套收齐,JS 就真正能「画」出一整张网页了。

约 22 分钟 6 个动手实验 DOM 增删四件套

一、createElement 与文本节点

想改 DOM 之前,先得造一个节点。浏览器给了三件工具: createElement 造元素、createTextNode 造纯文本、 createDocumentFragment 造一个「离线容器」。 造好的节点还不属于任何父元素,得手动插进去才会显示。

createElement(tag)
造一个元素节点:<div>、<li>、<span>…
createTextNode(text)
造一个文本节点:纯文本,没有标签
createDocumentFragment()
造一个「文档片段」——临时盒子,最后一次塞进去
孤儿节点
造出来没 append 之前,节点不在 DOM 树里,不显示
三 create 函数对照:造什么 / 返回值 / 何时用
函数造什么返回典型场景
createElement('div') HTML 元素节点 HTMLDivElement 造任意标签:li / p / button…
createTextNode('hi') 纯文本节点 Text 不想要任何标签、只塞文字时
createDocumentFragment() 文档片段(虚拟父节点) DocumentFragment 批量插入前先「攒一起」,一次塞入
刚造好的节点只是 JS 里的对象,没有进入 DOM 树—— 所以你刷新页面就看不见。必须调 append / appendChild 等插入方法, 它才真正「上墙」。

二、appendChild / append:插到末尾

parent.appendChild(child) 是最经典的「追加到末尾」API。 现代写法 parent.append(...nodes) 支持多参数、还能直接传字符串当文本。 注意:节点如果已经在 DOM 里,再 append 会被「搬家」, 不会复制——这常被用来做节点移动。

点两个按钮看 appendChild 与 append 的差异:appendChild 一次收一个节点,append 可一次塞两个还能夹字符串——改 text 改成你自己的名字
append 还能传字符串: parent.append('hello') 会自动创建一个文本节点塞进去。 而 appendChild('hello') 会直接报错——它只收节点。

三、insertBefore / before:精确插入

append 只会插到末尾。想插到指定元素前,得用 parent.insertBefore(newNode, refNode)——在 refNode 前面插入 newNode。 现代等价写法是 refNode.before(newNode),方向更直观: 「在这个节点前面加一个」。

点按钮在「第二个 li」前插入新 li:① 用经典 insertBefore(parent, ref);② 用现代 refNode.before() —— 改 newText 成你想插入的内容
小窍门:想插到末尾,没有 appendChild 也能写 parent.insertBefore(node, null)——第二个参数传 null,等价于追加末尾。 不过现代项目直接用 append 更直观。

四、removeChild / remove:删除节点

节点长大也会被砍。parent.removeChild(child) 是经典写法: 需要知道父节点。现代写法 child.remove() 更省心——直接喊「我自己走」,不必回头问爸爸。 被 remove 的节点还在 JS 里,只是脱离了 DOM 树,还能再 append 回去。

删除 API 对比:removeChild vs remove
API调用者参数现代项目用
parent.removeChild(child) 父节点 要删的子节点 兼容老浏览器时
child.remove() 要删的节点自己 无 现代标配,省去「找爹」一步
child.remove() 后再 append — — 节点对象还在 JS 内存里,可塞回 DOM
点按钮逐个删除:① removeChild 经典路径;② child.remove() 现代路径——观察输出框里「被删节点还在 JS 里」的事实
被删的节点并没有被销毁,JS 变量还在引用它。 想复活,直接再 append 回去就行——这是做「回收站」「撤销」功能的原理。

五、replaceChild / replaceWith:替换节点

节点说换就换。经典写法 parent.replaceChild(newChild, oldChild): 「父节点喊一声,把老的换成新的」。现代等价是 oldChild.replaceWith(newChild)——「老的说自己让位」, 参数顺序和名字都对得上动作。

点按钮把 #old 替换成 <b>新文本</b>:① 用 replaceChild;② 用 replaceWith —— 看两种写法最终结果一样
替换一个节点时,它原本的事件监听、属性、子节点都会被丢掉。 想保留旧的:先 parent.insertBefore(oldEl, newEl) 把旧的临时挪开, 再 append 进去;或者直接用第三方库做 diff(React/Vue 干的活)。

六、克隆节点:cloneNode

想复制一份现有节点?用 node.cloneNode(deep)。 传 true 深克隆(连子节点一起复制), 传 false 浅克隆(只复制外壳,里面是空的)。 克隆出来的节点仍是孤儿,必须手动 append 才会显示。

点按钮克隆一段卡片结构:① 深克隆(含子节点)→ 插到舞台;② 浅克隆(只克隆壳)→ 看 innerHTML 是空壳 —— 改 title 看新内容
克隆的 id 会重复! HTML 里 id 必须唯一,克隆节点如果不改 id 直接 append, 后面 getElementById 只会拿到第一个——记得克隆后立刻改名。

七、DocumentFragment:批量插入的性能秘密

往页面插 1000 个 <li>,直接循环 appendChild, 浏览器每插一个就重排一次(重新计算布局、重新绘制), 1000 次就是 1000 次重排——肉眼可见的卡顿。

正确做法:先把 1000 个 li 全部塞进一个DocumentFragment(文档片段), 再把这个片段一次性 appendChild 进去——浏览器只重排一次。

批量插入性能对照:1000 个 li 的耗时差距
方案重排次数适用场景
循环 appendChild N 次(每插一个一次) 几行时无所谓
Fragment + 一次 append 1 次 列表渲染、批量新增节点
cloneNode + Fragment 1 次 + 0 重排 模板复用(如聊天记录批量追加)
点按钮同时跑两路:① 1000 个 li 循环 appendChild(多次重排);② 用 Fragment 攒齐后一次 append(1 次重排)—— 看下面的耗时数字
DocumentFragment 是「一次性筷子」: appendChild 到真实 DOM 后,片段里的子节点会被搬走, 片段本身变空——所以可以反复用它攒节点、再批量提交。

八、本章小结

  • createElement / createTextNode / createDocumentFragment 三件套造节点,造出来都是「孤儿」,必须 append 才上墙。
  • append / appendChild 追加到末尾;append 可多参数、可传字符串,appendChild 只收节点。
  • insertBefore / before 插到指定元素前;现代 refNode.before(newNode) 方向更直观。
  • removeChild / remove 删除节点;child.remove() 不用找爹,是现代标配。被删节点对象仍在 JS 里,可再 append 复活。
  • replaceChild / replaceWith 替换节点;oldChild.replaceWith(newChild) 让旧节点自己让位。
  • cloneNode(true) 深克隆(含子节点)、cloneNode(false) 浅克隆(只壳);克隆节点仍要 append 上墙,记得改 id。
  • DocumentFragment 攒齐节点一次性 append,1 次重排代替 N 次——批量插入性能关键。
动手练习: ① 任务一:用 createElement + appendChild 拼一个迷你 todo list—— 一个输入框 + 一个按钮,按钮点击时把输入框内容追加成新 <li>; ② 任务二:用 DocumentFragment 批量插入 100 行表格行,每行一个从 1 累加到 100 的编号,用 performance.now() 测耗时并打印到 __out 里; ③ 任务三:给 todo list 的每一项加一个「删除」按钮,点击时调用 li.remove() 删除自己; ④ 任务四(挑战):先用 cloneNode(true) 克隆一个模板卡片,把克隆体的 id 改名后插入页面——观察 id 不改时 getElementById 会出什么问题。