ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

原生JS与jQuery中append添加div:从基本用法到性能优化

原生JS与jQuery中append添加div:从基本用法到性能优化 先说一个我自己的习惯只要遇到“往页面里动态加一块内容”我会先停下来想一下到底用 append还是用 innerHTML还是用 insertAdjacentHTML。不是因为功能上有难度而是这三者看起来都能做到但背后的行为差别很容易让人写出一堆埋着雷的代码。今天要聊的核心场景就是最基础、也最常被问的那一个用 JS 或者 jQuery 的 append 方法往容器里添加 div 元素。网上关于这个问题的版本很多但不客气地说大部分零散答案只告诉你“怎么写”没有告诉你“为什么这么写”。我打算把原生 JavaScript 和 jQuery 两条线都拆开从参数类型、返回值、兼容性、性能、事件绑定这些角度把 append 相关的知识一次讲透。无论你是刚接触前端的新手还是从 jQuery 时代一路走到原生 JS 的老开发这篇内容应该都能帮你少踩几个坑。1. 先搞清楚append 到底是在解决什么问题1.1 动态渲染需求为什么每天都有人要追加一个 div只要页面不是纯静态的前端就免不了动态创建元素。最常见的例子就是评论列表用户提交一条评论程序把内容拼成一条 div塞进列表容器底部聊天窗口收到一条消息生成一个气泡 div追加到消息流里面后台管理系统里点一下按钮渲染一张新卡片表单校验失败要在输入框下方插入一条错误提示……这些操作本质上都是同一个动作创建节点放进页面里位置通常在某个容器内部的末尾。所以 append 这个动作看起来只是几行代码实际承载的是“数据到视图”的桥接。很多人在搜索框里输入“js 添加 div”搜出来的是各种新旧混杂的写法比如innerHTML 、createElement、insertAdjacentHTML、jQuery 的.append()然后开始混淆明明都能加为什么别人推荐的方案和我写出来的效果不一样原因在于不同写法对“已有 DOM”的处理方式完全不同。innerHTML 会把容器内部所有内容重新解析一遍等于先清空再重建appendChild和append是在保留已有节点的前提下插入新节点insertAdjacentHTML则是把 HTML 字符串交给浏览器解析后插入到指定位置。这些差异直接关系到一个很现实的坑原有节点上绑定的事件会不会丢失、已经聚焦的输入框会不会失焦、页面会不会卡顿。1.2 append 在原生 JS 和 jQuery 中的角色定位原生 JavaScript 里的append是 DOM 规范提供的方法挂在Element原型上所以任何元素节点都可以调用。它做的事情很简单把一个或多个节点插到当前元素内部的最后位置。如果你传入的是字符串会被自动当成文本节点处理。这个设计比老牌方法appendChild更灵活因为它天然支持字符串和多个节点。jQuery 里的.append()则是封装过的便捷方法使用方式大体一致选中一个容器把内容添加到它的内部末尾。但在 jQuery 的世界里参数可以是 HTML 字符串、DOM 元素、jQuery 对象甚至是一个返回这些内容的函数。因为字符串会被自动解析成 DOM 元素所以很多开发者习惯直接写$(#container).append(div classcard.../div)省去了createElement和setAttribute的过程。你不需要把两套体系当成竞争关系。jQuery 的 append 本质上是建立在 DOM 操作之上的封装重点在于“选择器 链式调用”带来的书写便利。原生 append 胜在更底层、更可控、没有多余依赖。搞清楚各自的行为边界比争论谁更好用有意义得多。2. 原生 JavaScript 里 append 的正确打开方式2.1 基本语法与参数细节原生append的调用方式是parentElement.append(...nodes)。这里突出一个细节它可以接收多个参数。比如const container document.getElementById(list); const div1 document.createElement(div); div1.className item; div1.textContent 第一条; const div2 document.createElement(div); div2.className item; div2.textContent 第二条; container.append(div1, div2);这样container内部就会依次出现两个 div。如果是给表格添加行、给列表添加卡片这种一次插多块的能力比appendChild要顺手得多因为appendChild一次只能接收一个节点。还有一个容易忽略的用法直接传字符串。比如const container document.getElementById(box); container.append(div classtip注意这个行为/div);请特别注意这里字符串会被当作纯文本节点插入而不是被解析成 HTML。也就是说页面上会真的显示出一行“div classtip注意这个行为/div”文本而不是得到一个 div 盒子。很多人第一次用原生append踩坑就踩在这以为它和 jQuery 的 append 一样会自动解析 HTML 字符串。原生的是不会的想要解析字符串成 HTML得用insertAdjacentHTML或者innerHTML。如果你希望“用字符串创建 div 并插入”原生环境下更合理的写法是const container document.getElementById(box); container.insertAdjacentHTML(beforeend, div classtip这次是真正的 div/div);insertAdjacentHTML有两个参数第一个是位置beforeend表示插到容器内部末尾正好对应 append 的语义第二个是 HTML 字符串浏览器会把它解析成真实 DOM。所以原生环境里“insertAdjacentHTML beforeend”才是和 jQuery.append()最对等的写法。2.2 append 与 appendChild 的 5 个关键差异这两个方法名字太像初学者经常混用。它们的定位确实接近但运行时有明显区别。对比项appendappendChild是否支持字符串参数支持自动作为文本节点插入不支持传入字符串会报错一次可插入节点数支持多个参数每次只能插入一个节点返回值返回 undefined返回被插入的节点现代浏览器兼容较新规范老浏览器不支持几乎所有浏览器支持对 DocumentFragment 的处理使用后片段变为空同样会把片段清空节点移入目标这里重点讲讲返回值差异。appendChild返回的是被插入的节点所以常见的组合拳是const div document.createElement(div); div.className card; container.appendChild(div).textContent 新卡片;因为appendChild返回了div后面可以直接点属性。而append返回undefined不能这样链式操作。如果你在代码里写过container.append(div).textContent x运行时会直接报错提示你无法读取undefined的属性。这个点真的很常见我见过不止一次有人在这种地方排查半天。还有一个明显的差异在字符串参数上。如果你用appendChild时不注意类型传了一个字符串进去浏览器会直接抛NotFoundError因为appendChild只接受 Node 对象。而append对字符串做了特殊处理会当作Text节点。这个设计其实很聪明写文本内容时直接container.append(加载中...)就能更新状态提示不用额外创建 Text 节点。不过在兼容性上appendChild更稳。老项目要兼容低版本系统自带浏览器时优先用appendChild更保险现代浏览器环境直接上append没问题。如果担心兼容又馋字符串参数可以先对非 Node 值做判断再决定走哪个方法。2.3createElement、className、textContent的组合套路创建 div 最稳的路线不是直接拼 HTML 字符串而是先用createElement创建元素节点再设置属性和内容最后 append 进页面。这样每一步都明确调试时也清楚问题出在哪。function createCard(title, desc) { const div document.createElement(div); div.className card; const titleEl document.createElement(h3); titleEl.textContent title; const descEl document.createElement(p); descEl.textContent desc; div.append(titleEl, descEl); return div; } const list document.getElementById(cards); list.append(createCard(标题一, 描述一)); list.append(createCard(标题二, 描述二));这里为什么推荐textContent而不是innerHTML因为textContent会把字符串里的 HTML 标签、特殊符号全部当作文本处理。如果你用innerHTML拼接用户输入的内容比如titleEl.innerHTML title用户一旦在标题里输入img srcx onerroralert(1)之类的字符串就有可能被浏览器当脚本执行造成 XSS 风险。哪怕只是内部系统的后台也不该养成直接用innerHTML拼用户数据的习惯。实际做项目时我的习惯是凡是内容来自接口或用户输入一律用textContent或createTextNode凡是内容是开发自己写死的静态结构比如“按钮 图标 文字”这种模块才考虑用insertAdjacentHTML直接拼 HTML 字符串。这样在安全性上会比较省心。3. jQuery 里 append 的灵活用法3.1 三种常用传参方式与适用场景jQuery 的.append()返回的是 jQuery 对象本身所以支持链式写法。它接受以下类型的参数第一种传 HTML 字符串$(#list).append(div classitem动态项/div);这种写法最简单适合结构固定、不涉及用户输入的模块。jQuery 会把字符串解析为 DOM 节点后插入。注意如果你要拼接变量要留意转义问题。比如const name $(#nameInput).val(); $(#list).append(div classitem name /div);用户输入的值如果含有、、引号可能破坏 HTML 结构甚至引发安全问题。jQuery 没有自动转义这一说字符串里写了什么就是什么。更稳妥的做法是先用 DOM API 生成节点再把文本值塞进去最后把节点作为参数传给 append。第二种传 DOM 元素或 jQuery 对象const div document.createElement(div); div.className item; div.textContent 原生创建的 div; $(#list).append(div);这个写法允许原生 JS 和 jQuery 混用在同一条流程里。jQuery 收到 DOM 元素后会把它包装成 jQuery 对象再插入。还有一种更常见的场景是用 jQuery 选择器选中已有的元素然后移动$(#btn).on(click, function() { $(#list).append($(#templateDiv)); });这里会把templateDiv这个节点从原来所在的位置移动到#list内部末尾。要注意的是append 移动的是这个节点本身原来位置的节点会消失。如果只是需要复制一份应该用.clone()$(#list).append($(#templateDiv).clone());第三种传函数$(#list).append(function(index, html) { return div classitem第 index 个/div; });回调函数里的index是当前容器内已有子元素的数量html是容器当前的 innerHTML 字符串。这种写法适合在追加的同时按已有子元素数量生成不同内容但坦白说用得不多。大部分场景直接用字符串拼接或对象插入就够了。3.2 appendTo、prepend、before、after 的方向选择jQuery 里有一组很容易绕晕的方法。它们的用词很像但插入位置各不相同。.append(content)把内容追加到目标元素内部末尾。.appendTo(target)功能和 append 相同只是主宾关系反过来。$(#list).append(div)等价于$(div).appendTo(#list)。.prepend(content)把内容插入到目标元素内部开头。.before(content)把内容插入到目标元素前面作为兄弟节点。.after(content)把内容插入到目标元素后面作为兄弟节点。我画个简单场景。假设有个容器div idbox span旧内容/span /div$(#box).append(bnew/b)结果是span旧内容/spanbnew/b$(#box).prepend(bnew/b)结果是bnew/bspan旧内容/span$(#box).after(bnew/b)结果是div idbox.../divbnew/b追加在盒子后面$(#box).before(bnew/b)结果是bnew/bdiv idbox.../div追加在盒子前面append 和 appendTo 的方向区别本质上是调用主体不同。写插件或封装组件时如果返回值要支持链式调用我通常更喜欢appendTo因为它在追加操作之后还能继续操作被插入的那个元素$(div classcard新卡片/div) .appendTo(#cards) .css(background, #f5f5f5) .on(click, function() { console.log(点击卡片); });这种写法在动态创建弹窗、工具条时非常舒服先构造元素、再插入、再绑定事件和样式一气呵成。3.3 jQuery 内部大致做了什么jQuery 的 append 底层其实也是调用了 DOM 的节点插入能力但在此之前它会做一层参数处理。如果是 HTML 字符串jQuery 会用自身的buildFragment机制把字符串解析成 DocumentFragment然后再把片段中的节点依次插入目标容器。这也回答了很多人问的一个问题为什么 jQuery 的 append 能直接解析 HTML 字符串而原生 append 不能因为 jQuery 替你把字符串转成了 DOM 节点。理解这层逻辑有什么好处你会明白 jQuery 解析字符串时是有开销的。如果在一个循环里反复$(#list).append(div.../div)每次 append 都会经历“字符串解析 DOM 插入 页面重排”循环一百次就是一百次完整流程页面卡顿几乎是必然的。后面我会专门讲性能优化方案。4. append 实战场景拆解从列表渲染到动态选择4.1 场景一动态列表的两种写法最常见的需求是输入框加按钮点一下往列表里追加一条数据。原生 JS 的完整实现大概是这样的!DOCTYPE html html head meta charsetUTF-8 title动态列表/title /head body input idinput placeholder输入内容 / button idaddBtn添加/button div idlist/div script const list document.getElementById(list); const input document.getElementById(input); const addBtn document.getElementById(addBtn); function addItem() { const value input.value.trim(); if (!value) return; const div document.createElement(div); div.className item; div.textContent value; list.append(div); input.value ; input.focus(); } addBtn.addEventListener(click, addItem); input.addEventListener(keydown, function(e) { if (e.key Enter) addItem(); }); /script /body /html用 jQuery 写的话代码会短不少$(#addBtn).on(click, function() { const value $(#input).val().trim(); if (!value) return; $(#list).append(div classitem value /div); $(#input).val().focus(); });两种写法都能跑。但如果你仔细看 jQuery 版本会发现value直接拼进了 HTML 字符串。假如用户输入的是你好 img src... 世界字符串拼接处的 HTML 结构就会被干扰。所以在生产项目里遇到用户输入型数据我即使用了 jQuery也倾向于用“原生创建节点 jQuery 插入”的方式const div document.createElement(div); div.className item; div.textContent value; $(#list).append(div);这样既借用了 jQuery 的便利又避免了字符串拼接的安全隐患。4.2 场景二循环出来的 div一行两个怎么选中最后两个元素这个场景在热搜词里也出现了确实非常典型。通常我们用一个容器把若干个 div 循环渲染出来比如每个 div 宽度 50%形成一行两个的网格。需求经常是最后一行如果只有两个元素希望最后两个 div 有特殊视觉效果或者不给最后两个加某些伪类样式。先说选择方式。原生 JS 里可以这样const cells document.querySelectorAll(.grid .cell); if (cells.length 2) { cells[cells.length - 2].classList.add(last-two); cells[cells.length - 1].classList.add(last-two); }注意是从后往前索引因为querySelectorAll返回的是 NodeList可以通过索引访问。如果你要选的是“最后一行”而这一行可能不满两个那么上面这个“最后两个元素”的写法就正好覆盖了单数和双数的两种情况。jQuery 里可以配合:last和prev()const $cells $(.grid .cell); if ($cells.length 2) { $cells.last().addClass(last-two); $cells.last().prev().addClass(last-two); }还有一种更省事的方式是直接用 CSS 的:nth-last-child.grid .cell:nth-last-child(-n2) { /* 最后两个 div 的样式 */ }-n2的意思是从倒数第一个开始往回数两个。写成公式就是当 n 从 0 开始递增时选择倒数第 1 个和倒数第 2 个。如果你需要这两块不加伪类元素可以配合伪元素去掉.grid .cell::after { content: ; display: block; } .grid .cell:nth-last-child(-n2)::after { content: none; }不过要小心如果.grid里除了.cell还有别的子元素:nth-last-child会以所有兄弟元素计数:last-child也只能匹配真正的最后一个子元素。如果结构不干净建议后端或 JS 生成时直接给最后两项加一个类名像is-last-row比伪类选择器更稳。4.3 场景三新添加的 div 应该如何绑定事件这是 append 最经典的翻车现场。新手常写成$(.item).on(click, function() { console.log(点击); });然后动态 append 出来的.item点击没有任何反应。原因很简单上面这行代码在页面初始化时执行当时 DOM 里还不存在 append 出来的那些.item所以$(.item)只选中了原有的少量节点后面新加的节点根本不在事件绑定范围内。解决方式是事件委托。把监听器绑定到这些节点的稳定父容器上利用事件冒泡机制捕获子元素触发的事件。jQuery 写法$(#list).on(click, .item, function() { console.log(点击, $(this).text()); });原生写法document.getElementById(list).addEventListener(click, function(e) { const item e.target.closest(.item); if (item) { console.log(点击, item.textContent); } });closest(.item)会在冒泡路径上从当前目标往上层找直到找到匹配.item的元素。这样不管之后 append 多少个 div只要它们都在#list内点击事件都能被捕获。我在实际项目里几乎都用这种方式处理动态列表的交互。4.4 场景四根据 name 或索引操作特定 div动态创建的元素多了以后怎么快速定位某个 div 是常见需求。可以用 setAttribute / 属性直接设置 nameconst div document.createElement(div); div.setAttribute(name, userCard); div.className card; div.textContent 用户卡片; container.append(div);原生查询const card document.querySelector(div[nameuserCard]);jQuery 查询const $card $(div[nameuserCard]);但这里要提醒一句name 属性并不是 div 的标准属性虽然浏览器允许设置和查询语义上不推荐。如果只是为了做标识更合适的方式是用>div.setAttribute(data-id, user123);查询时用document.querySelector(div[data-iduser123]);jQuery 也可以用同样的属性选择器。开发中我比较建议用>for (let i 0; i 200; i) { const div document.createElement(div); div.className item; div.textContent 内容 i; document.getElementById(list).append(div); }200 个 div 不多但每一次 append 都会触发一次强制同步布局。想要更稳应该尽量把创建节点和插入页面的动作分开先在内存里把所有节点准备好最后一次插入。这就是 DocumentFragment 的用武之地。DocumentFragment 是一个轻量级文档片段它存在于 DOM 之外往里面添加节点不会触发页面渲染组装完成后一次性插入目标容器const list document.getElementById(list); const fragment document.createDocumentFragment(); for (let i 0; i 200; i) { const div document.createElement(div); div.className item; div.textContent 内容 i; fragment.append(div); } list.append(fragment);list.append(fragment)之后fragment 里的节点会被移入#list而 fragment 本身变为空。这个方案在原生 JS 批量渲染时几乎是标配。jQuery 环境下的对应思路是避免在循环里反复调用.append()而是先拼好 HTML 字符串或一个数组最后一次插入let html ; for (let i 0; i 200; i) { html div classitem内容 i /div; } $(#list).append(html);如果内容不涉及用户输入拼接字符串这种方式在 jQuery 里最直接也最高效。要注意的是字符串拼接在性能上也有点讲究但 200 条以内的拼接对前端来说无感真正该避免的是反复进 DOM。5.2 append 和 innerHTML、insertAdjacentHTML 怎么组合原生 JS 里append 适合“创建节点对象并插入”的场景。如果要画一大段静态 HTML再配一点动态参数用insertAdjacentHTML往往更顺手const container document.getElementById(container); container.insertAdjacentHTML(beforeend, div classcard h3${title}/h3 p${desc}/p /div );模板字符串可以帮助多行结构可读性更高。这个方案有几个优点浏览器直接解析字符串生成 DOM不会有反复createElement的痛苦也不会像innerHTML 那样重建整个容器破坏已有 DOM 节点和事件。一定要避免的写法是container.innerHTML divxxx/div。这个操作会先把容器内部所有现有 HTML 序列成字符串再拼上新内容最后重新设置 innerHTML。过程非常重而且原来的子元素全部会被销毁重建已绑定的事件全部丢失正在操作的输入框会失焦血泪教训不少。5.3 注意“追加”和“替换”的语义差异append 是追加html()是替换。jQuery 里这两个概念经常被混用。假设你执行了$(#list).append(div1/div)又执行$(#list).html(div2/div)结果是容器里只剩下 2 这条数据之前 append 的那些全部没了。所以当你需要增量添加时务必保持 append只有当你明确要重置整个容器内容时才该用 html()。还有一个容易被忽视的细节用append插入的元素如果已经在页面其他地方存在它会被移动而不是复制。原位置会消失。很多人以为 append 是复制结果发现原来的元素“跑”过去了。这不算 bug是 DOM 规范定义的行为。需要保留原元素时先.clone()再 append。5.4 常见问题与排查速查表现象可能原因解决思路页面新增了 div 但不显示容器有固定高度且 overflow hidden或元素在视口外检查样式、滚动位置打开开发者工具查看元素是否存在用原生 append 拼 HTML 字符串页面上显示了源码文本原生 append 不解析 HTML 字符串只当文本节点改用 insertAdjacentHTML 或先 createElementjQuery append 后点击事件没反应事件绑定在 append 之前新元素未被选中改用事件委托$(parent).on(click, .item, fn)追加多个相同节点后旧节点不见了直接把已有节点 append 到新位置节点被移动需要复制时使用.clone()循环 append 后页面越来越卡大量同步 DOM 插入导致频繁 reflow使用 DocumentFragment 或拼接字符串后一次插入动态生成的 div 样式错位一行两个变成一行一个父容器没有设置 flex 或 grid子元素宽度不含盒模型边界检查布局与 box-sizing 设置追加的内容被后续 html() 清空代码里在别处使用了 html() 重置容器统一用 append 维护增量内容或者在 html() 后重新渲染数据我在项目里排查这类问题有一个固定习惯先在浏览器开发者工具的 Elements 面板确认节点到底有没有进去。节点没进去问题在 JS 逻辑节点进去了但看不见问题在 CSS节点进去了但事件没响应问题大概率在事件绑定时机。按这个顺序排查能省下一大半时间。6. 几个值得记住的实操细节6.1 ajax 加载列表后 append 的典型代码结构实际项目里append 经常和 ajax 请求配合使用。请求接口拿到数据遍历数据把每条数据渲染成 divappend 到列表容器。这段流程的原生 JS 写法大概是async function loadList() { const container document.getElementById(list); container.innerHTML ; // 先清空 const res await fetch(/api/list); const data await res.json(); data.forEach(function(item) { const div document.createElement(div); div.className card; div.dataset.id item.id; const title document.createElement(h4); title.textContent item.title; div.append(title); container.append(div); }); }jQuery 时代则是function loadList() { $(#list).empty(); $.get(/api/list, function(data) { if (!data || !data.length) return; let html ; data.forEach(function(item) { html div classcard>let div; for (let i 0; i 5; i) { div document.createElement(div); div.textContent 第 i 项; list.append(div); }这种写法没问题因为每个循环都会重新赋值给div并且 append 的是节点引用不是变量名。但是如果你把变量声明在循环体外并且误用了闭包场景就可能出现诡异问题。最常见的陷阱是在循环里给元素绑定事件时用了varfor (var i 0; i 5; i) { const div document.createElement(div); div.textContent 第 i 项; div.addEventListener(click, function() { console.log(点击, i); }); list.append(div); }如果这里用var i所有点击事件最终打印的都是同一个值也就是循环结束后的 i。现在写let能避免大部分问题但我在历史项目里见过太多 var 闭包坑每次都会注意检查循环里的事件回调有没有捕获到了正确的索引。6.3 动态 div 的 alt、title、data-* 属性最好在创建时设置append 之后再 setAttribute 也能生效但推荐在创建节点时就设置完整属性减少后续 DOM 操作次数。比如const div document.createElement(div); div.className card; div.dataset.id item.id; div.title item.name; div.setAttribute(role, listitem);创建时一次性配置好不仅性能好可读性也高。特别是dataset浏览器会自动把>$(div classtoast保存成功/div) .appendTo(body) .fadeIn(200) .delay(2000) .fadeOut(400, function() { $(this).remove(); });proceed这样写就很顺畅用 append 的话你还得先把创建的节点存进变量再手动找到它绑定动画代码会啰嗦很多。原生环境下我的习惯是创建一个轻量帮助函数统一处理“创建 div 并追加”这件事function appendDiv(parent, className, text) { const div document.createElement(div); div.className className; if (text) div.textContent text; parent.append(div); return div; }这个函数返回新创建的 div后续要改内容、绑事件、加动画都能直接操作。项目里如果经常要动态加卡片、广告位、提示条提前封装这么一层能省非常多重复代码。最后再强调一句不管用原生还是 jQuery动态创建 DOM 都尽量遵循“先在内存里准备好再一次性插入”的思路。遇到性能问题先检查循环体遇到事件失效先检查绑定时机遇到样式不对先开开发者工具确认元素状态。这几个检查点基本能覆盖 append 相关的绝大多数坑。实战经验攒起来之后你会发现 append 这件小事其实涵盖了前端 DOM 操作的一半精髓。
返回列表