最近在开发群里看到有人连续追问一个问题JS和jQuery中如何用append方法添加div元素乍一看这像是入门第一周的作业可等到自己动手写时才发现一个简单的append背后能扯出动态创建节点、插入位置、事件绑定失效、大批量渲染性能、循环结构里怎么定位最后几个元素等一大堆连锁问题。这篇文章就把我在实际开发中围绕append加div积累的经验全部摊开。不管你是刚开始学前端想弄懂原生JS写法还是在维护老项目天天用jQuery或者只是偶尔写点动态列表、表单行、弹窗内容这篇文章都值得花几分钟看完。我会从最基础的createElementappendChild说起再讲jQuery的append及它和appendTo、prepend、before、after的区别最后落地到几个真实踩坑场景和可以直接抄的封装代码。1. 为什么明明用innerHTML就行我还要专门整理append的用法很多初学者看到往页面里添加div这个需求第一反应都是改容器的innerHTML。我最早也是这么干的比如要渲染一个卡片列表var list document.getElementById(list); for (var i 0; i 10; i) { list.innerHTML div classcard第 i 张卡/div; }这段代码在数据量小的时候一点问题没有但一旦数据量上来麻烦就跟着来了。1.1 innerHTML 的隐藏问题整容器重建list.innerHTML ...这行代码看着只是追加实际上浏览器做的是把list当前所有内容重新序列化成字符串然后在内存里拼接上新内容再整体解析、整体替换。也就是说list下面原本已经存在的一堆div节点全部被销毁重建了一遍。这会造成两个实际后果原有节点上绑定的事件会丢失。比如列表里某个div上绑了个click事件容器innerHTML被重写一次之前绑定的那个节点已经被销毁了事件自然也随着消失。常见现象就是列表第一次点击正常刷新数据后再点没反应。用户输入状态会丢失。如果列表里有input输入框用户刚往里面打了一行字容器被重建后输入内容直接没了。这个问题在动态表单、可编辑表格里尤其致命。1.2 性能问题一次追加变成全量重绘每执行一次innerHTML 页面都会触发一次相对完整的重排和重绘。循环10次就是10次看起来还好循环1000次甚至更多页面的卡顿就非常明显了。你本来只想加一个新div页面却把整个列表重新渲染了一遍浪费掉的性能全在无意义的重建上。1.3 append这一类方法真正解决的问题append方法的本质是增量追加我只在现有DOM树上添加一个新节点不影响已经存在的任何节点。原本绑定的事件还在用户输入的内容还在页面也不需要整体重绘浏览器只需要把新节点插进去计算一次布局。所以在什么场景下应该果断用append而不是innerHTML动态加载列表比如点击加载更多分页数据不断追加。异步请求返回后插入新节点请求结果回来把新卡追加到列表尾部。动态表单行用户点一个按钮就新增一行输入框。弹窗、提示条、树形菜单懒加载子节点这些场景的共同点都是在已有内容基础上新增节点。理解了这一层再来看如何用append添加div这个问题你就知道它不只是一个API怎么调的问题而是涉及什么时候该用、怎么用才不会埋坑的一套完整姿势。2. 原生JS从document.createElement到appendChild/append的完整姿势先看原生JS。为什么先说原生因为jQuery底层也是封装的原生DOM API你把原生的弄明白jQuery那个就是套了一层壳。2.1 最基础的三步新建、填入、挂载往页面里添加一个div原生JS的标准做法是三步走// 1. 创建div节点 var newDiv document.createElement(div); // 2. 给div设置内容和属性 newDiv.className card; newDiv.textContent 这是一张新的卡片; // 3. 把div挂载到目标容器里 document.getElementById(container).appendChild(newDiv);第一步createElement(div)只是创建了一个游离的节点。它已经存在内存里但还没有进入页面DOM树你肉眼看不到它。很多初学者写完createElement之后以为页面会立刻出现新div然后到处找哪里出了问题——其实问题不在创建而在于你忘了最后一步挂载。第二步设置内容有两种选择textContent和innerHTML。div.textContent 这是纯文本; div.innerHTML strong我是一段HTML结构/strong;textContent会把内容当作纯文本处理就算里面写了div也不会被解析只会显示成大括号里的字符串。它的好处是安全不会因为数据里的、破坏页面结构。innerHTML则会把字符串当成HTML代码解析适合需要插入一段成型结构的情况但如果你把用户输入直接塞进innerHTML就存在被注入脚本的风险。在真实项目中我一般遵循一条原则只要是动态数据默认用textContent只有确定字符串里是可信的、固定的HTML模板时才用innerHTML。除了className开发中常用的还有div.id card- id; div.classList.add(active); // 追加样式类 div.dataset.index 3; // 存自定义数据 div.style.marginTop 10px; // 设置行内样式这些都是创建div之后顺手要做的操作面试和实际开发中都高频出现。2.2 appendChild和append两者到底有什么区别原生JS里其实有两个追加方法appendChild和append。很多新人以为它们是同一个东西实际差别不小对比点appendChildappend返回值返回被插入的节点方便继续链式操作返回undefined参数类型只接受DOM节点Node可以接受DOM节点也可以接受字符串多节点插入一次只能插一个可以一次插入多个节点或多个字符串兼容性老到IE6都能用现代浏览器支持IE不支持// appendChild只能传节点 var div document.createElement(div); container.appendChild(div); // append可以把字符串当作Text节点插入 container.append(div我是普通字符串/div);注意append接收字符串时这个字符串会以文本节点的形式插入而不是被解析成HTML。也就是说container.append(div我是普通字符串/div);页面上显示出来的就是一个div我是普通字符串/div的纯文本而不是一个真实的div元素。这一点和jQuery完全不同很多人从jQuery切到原生JS就在这栽了跟头。多个节点同时插入是append的独有能力var div1 document.createElement(div); var div2 document.createElement(div); container.append(div1, div2, 一段文字);既然appendChild的兼容性更好、还能返回节点那为什么还需要append核心原因是多参数和字符串支持让写起来更方便。实际项目中你可以优先用appendChild保证兼容也可以根据运行环境用append这个没有绝对对错。2.3 一次性循环追加100个div为什么页面会卡当你的业务逻辑变成根据数组渲染一堆卡片时最容易写出这样的代码var data [/* 几百条数据 */]; data.forEach(function (item) { var div document.createElement(div); div.className card; div.textContent item.name; container.appendChild(div); });功能上完全没问题但如果数据量上百甚至上千能明显感觉到页面渲染变慢。原因是我上面说的每次都往真实DOM里插入一个节点浏览器就得重新计算一下布局。循环多少次就触发多少次重排。解决方案是用DocumentFragment一个虚拟的临时容器var fragment document.createDocumentFragment(); data.forEach(function (item) { var div document.createElement(div); div.className card; div.textContent item.name; fragment.appendChild(div); }); container.appendChild(fragment);所有新div先塞进fragment最后一次性把fragment挂到真实DOM上。浏览器只需要做一次整体插入渲染速度大幅提升。我之前在老项目里渲染一张1000行的表格改成fragment之后渲染耗时几乎缩短了一个数量级。但这里也提醒一句如果只是追加三五个div就不要用fragment直接appendChild就好。为了微小的性能提升让代码变得复杂属于过度优化。2.4 如果手里有一段现成的HTML字符串用insertAdjacentHTML还有一种常见需求后端接口返回了一段已经拼好的HTML你只想原样插进某个位置。这时候用createElement再一个个设置反而麻烦。更合适的是insertAdjacentHTMLcontainer.insertAdjacentHTML(beforeend, div classcard直接插入/div);它的四个位置参数很直观beforebegin插到容器自身前面afterbegin插到容器内部开头beforeend插到容器内部结尾afterend插到容器自身后面和innerHTML 相比insertAdjacentHTML只解析和插入新增的部分不会重建容器原有内容性能和安全性都更好。它们的分工也很清楚需要拿到节点本身继续操作比如加事件、改样式→ 用 createElement appendChild手里有一整段可信HTML字符串只想快速塞进去 → 用 insertAdjacentHTML3. jQuery的append方法从语法到往哪儿插一次讲明白jQuery的append本质就是封装了原生DOM的插入操作但它给使用体验做了很多优化。很多老项目里到现在还是jQuery写的一套动态渲染逻辑你要是接手这样的项目这些知识点绕不开。3.1 基本语法和参数类型jQuery里往一个容器追加div最简单的写法是一行$(#container).append(div classcard新增卡片/div);这里传入的HTML字符串会被浏览器解析成真实节点插入容器。和原生append字符串变成文本不同jQuery的append会按HTML解析。这也是为什么很多从jQuery入门的人切到原生JS时会觉得明明写法差不多为什么效果不一样。append支持三类参数// 1. HTML字符串 $(#container).append(div字符串写法/div); // 2. DOM节点 var div document.createElement(div); div.textContent 原生节点写法; $(#container).append(div); // 3. jQuery对象 $(#container).append($(#someTemp));还有一种容易被忽略的用法传一个函数。var index 0; $(#container).append(function () { return div classcard第 (index) 个项目/div; });append里的函数会在每次追加时被调用返回值就是要插入的内容。这个特性在根据索引生成有规律div的场景里很实用。这里也能顺带理解JS中函数是对象这句话在实践里的意义——函数可以作为参数传进另一个函数里。3.2 append、appendTo、prepend、before、after的插入位置与选型jQuery里跟插入位置相关的API特别容易混淆我每次带新人都会用下面这张表把位置关系理清楚方法作用示例append(content)把内容追加到目标容器内部末尾$(#box).append(A)prepend(content)把内容插入到目标容器内部开头$(#box).prepend(B)before(content)把内容插到目标元素前面兄弟位置$(#box).before(C)after(content)把内容插到目标元素后面兄弟位置$(#box).after(D)appendTo(target)把当前内容追加到target内部末尾写法与append反着来$(E).appendTo(#box)一套写法看下来append和appendTo最容易让人晕。区别只在于谁调用谁、谁是谁的目标// 等价写法 $(#container).append(div内容/div); $(div内容/div).appendTo(#container);当你在代码里已经有一个div变量要决定把它放到哪儿时用appendTo读起来更顺。比如树形菜单懒加载var $childDiv $(div classtree-node子节点/div); $childDiv.appendTo($parentNode);比写$parentNode.append($childDiv)更像是在描述这个子节点要挂到父节点下面语义更清晰。而绝大多数固定场景里$(#container).append(...)已经够用。另外提醒一个细节jQuery大部分方法都返回原来的jQuery对象所以可以一直链式调用$(#container) .append(div第一个/div) .append(div第二个/div);每次调用append都是在上一次的结果后面继续追加最后页面里会按顺序出现两个div。3.3 jQuery动态加div时事件绑定为什么经常没反应用jQuery开发时最经典的问题就是我为动态生成的div绑定了click事件结果点击根本没反应。这个坑我建议从绑定时机和事件委托两个角度理解。错误的写法// 页面初始化时div还不存在 $(.card .delete-btn).on(click, function () { $(this).closest(.card).remove(); }); // 之后才动态append进div $(#container).append(div classcardbutton classdelete-btn删除/button/div);这段代码的问题在于绑定事件时页面上根本没有.card .delete-btn这个元素jQuery执行on的时候只给当前已存在的节点绑定。后来append出来的div自然没有事件。正确的做法是事件委托——绑定到始终存在的父容器上让事件从子节点冒泡上来$(#container).on(click, .card .delete-btn, function () { $(this).closest(.card).remove(); });原理是不管以后往#container里追加多少个div只要点击发生在容器内部事件就会一路冒泡到容器上被jQuery识别后判断target是否匹配选择器匹配了才触发回调。原生JS里也有对应写法document.getElementById(container).addEventListener(click, function (e) { var btn e.target.closest(.delete-btn); if (btn) { btn.closest(.card).remove(); } });关于事件这块我的建议是凡是动态添加的节点一律用事件委托绑定。这样既不需要担心绑定时机也给后续新增节点省去重复绑定的麻烦。如果多个动态事件叠加在同一个容器上回调里执行前先做一次选择器判断基本能避开所有事件没反应的坑。4. 动态追加div最容易翻车的三个场景事件失效、引号嵌套、重复追加调API人人会真正的开发难点都在组合上。我把自己实际工作中踩过的三个高频场景完整拆开每个场景都会给出排查思路和最终方案。4.1 场景一事件失效的完整排查链路有个朋友在自己的管理系统里写过一个动态表单行功能是点添加指标按钮就往表格里append一个新行。他当时遇到的问题很有代表性第一次点按钮新行里的删除按钮能删掉自己第二次、第三次点击后新行里的删除按钮完全没反应。完整排查链路是这样的第一步先确认节点是否真的插入了。在浏览器控制台里执行document.getElementById(container).innerHTML看动态行是否已经存在于页面上。如果根本没有节点那是append没执行或者append到了别的容器里比如选择器写错。第二步确认绑定代码执行顺序。检查$(.delete-btn).on(click)是在append之前写的还是之后写的。如果在append之前基本就是这个原因。顺手让他改成事件委托到父容器上。第三步确认事件是否叠加。如果他在每次ajax回调里都执行一次$(#container).off().on(click, ...)第一次没问题第二次事件被解绑又重新绑看似正常实则很容易漏掉其他事件。正确做法是把委托绑定放在初始化阶段只绑一次不要放到每次回调里。第四步检查目标选择器是否被外部样式或其它节点干扰。我用控制台点开Event Listeners面板确认click绑定的target确实是要找的元素发现他的选择器.del-btn在后续拼接HTML时Class名加错了写成.delBtn导致委托匹配不上。这四步走完问题基本都能定位。如果你在项目里也遇到动态div事件没反应强烈建议按这个顺序查而不是上来就改代码猜。4.2 场景二字符串拼接的引号嵌套地狱用append拼接HTML时最难看、最容易错的代码就是字符串里套引号。最常见的是这种做法var id abc123; var s div onclickdel( id )删除/div; $(#container).append(s);id如果是数字这段代码还能凑合运行一旦id是字符串最终生成的HTML变成onclickdel(abc123)浏览器直接报错。于是有人开始疯狂转义var s div onclickdel(\ id \)删除/div;看着都眼晕维护起来更是灾难。我的解决方案是双管齐下。方案一用ES6模板字符串至少让拼接可读var s div onclickdel(${id})删除/div;方案二也是我更推荐的不要在HTML里写内联onclick改用事件委托加data属性。var s div classdel-row>$(#searchBtn).on(click, function () { var list searchData(); list.forEach(function (item) { $(#result).append(div classitem item /div); }); });第一次搜索正常第二次搜索结果变成了两组div第三次变成了三组。问题本质是每次搜索都应该用新结果替换旧结果而append的语义是追加。正确的做法是每次渲染前先清空容器$(#searchBtn).on(click, function () { var list searchData(); $(#result).empty(); // 先清空上一次的结果 list.forEach(function (item) { $(#result).append(div classitem item /div); }); });原生JS对应写法是container.innerHTML 。但有一种情况不能无脑清空分页加载加载更多场景。这时每次点击都带着历史数据继续向后翻页不能empty只能append。这种场景要注意幂等性——同一个数据项不能因为重复请求被插入两次。我习惯在append前检查一下数据ID是否已经存在于页面$(#result).find([data-id id ]).length 0判断到已经存在就跳过避免重复渲染。5. 循环出来的div想只处理最后几个CSS伪类和JS索引怎么配合前面说了那么多基础用法现在聊一个真实需求循环出来的div一行摆放两个最后一排可能只剩一个或两个。这时候要给最后几个元素特殊样式比如不加分隔线、不加下边距。这个问题表面上是CSS实际在动态渲染场景里非常普遍。5.1 CSS伪类的写法last-child和nth-last-child假设每个div的class都是item容器是#list要在最后两个item上不加底部边框最直接的是写#list .item:nth-last-child(-n2) { border-bottom: none; }nth-last-child(-n2)的意思是从后往前数倒数第1个到倒数第2个正好就是最后两个元素。如果想精确处理最后一个用:last-child如果是要处理最后一行每行两个div就可以用:nth-last-child(-n2)。这些伪类看起来容易实际踩坑点是容器里除了item之外不能有其它元素兄弟节点。比如你在循环生成div之后又在容器末尾append了一个加载中的占位div那么:last-child匹配到的就是占位div而不是你想要的最后一个item。这种情况下要么把占位div放到另一个容器里要么改用JS索引判断。另外要区分:last-child和:last-of-type。:last-child只看它是不是父元素的最后一个孩子:last-of-type看是不是同类型元素的最后一个。当容器里混着div、span、p时这两个选择器的结果差别很大。5.2 JS索引判断的稳妥写法当CSS伪类受限于DOM结构没法工作时回到JS里用索引判断是最稳的。在循环生成div的代码里动态给最后两个div加一个类var items [a, b, c, d, e]; var total items.length; var lastStart total - 2; items.forEach(function (item, index) { var div document.createElement(div); div.className item; if (index lastStart) { div.classList.add(no-bottom); } div.textContent item; container.appendChild(div); });这样对应CSS只需要写.item.no-bottom { border-bottom: none; }有一个细节必须提醒当total等于1时lastStart是-1循环里所有index -1都成立会把唯一的div也加上no-bottom。如果这不符合预期要在代码里加一个边界判断if (total 2) { // 全部元素都是最后一行是否加类取决于业务需要 }5.3 动态div最好带索引或data字段在循环生成div时我强烈建议每个div都带上自己的数据和索引信息。不只是为了最后两个元素这个需求更是为了后续事件处理和状态同步。比如用data-index记录它在数组里的序号div.dataset.index index; div.dataset.id item.id;事件委托里就能准确找到对应数据container.addEventListener(click, function (e) { var itemEl e.target.closest(.item); if (!itemEl) return; var index itemEl.dataset.index; var id itemEl.dataset.id; // 用index和id去更新数据、请求接口都行 });很多动态div出来后不知道是哪条数据的问题都是因为在创建节点时没把业务ID存进去等想去找对应数据时只能靠猜。6. 把添加div封装成通用函数少写重复代码实际项目里添加div的场景往往不止一处。如果在十几个地方各写一遍createElement、setAttribute、appendChild代码会非常啰嗦。我习惯把添加div封装成一个小函数既保证调用处简洁也避免改样式时东改一处西漏一处。6.1 原生JS版通用函数function appendDiv(container, options) { var div document.createElement(div); if (options.id) { div.id options.id; } if (options.className) { div.className options.className; } if (options.text) { div.textContent options.text; } if (options.html) { div.innerHTML options.html; } if (options.data) { Object.keys(options.data).forEach(function (key) { div.dataset[key] options.data[key]; }); } if (options.style) { Object.keys(options.style).forEach(function (key) { div.style[key] options.style[key]; }); } container.appendChild(div); return div; }调用方式var newDiv appendDiv(document.getElementById(container), { className: card, text: 用户张三, data: { id: 1001, role: admin } }); newDiv.classList.add(active);返回div对象这个设计很重要因为你可能创建完之后还要继续操作它比如追加样式类、继续往里加子节点。封装函数把创建挂载做了把后续操作还给调用者。6.2 jQuery版封装jQuery项目里可以封装得更薄function appendDiv($container, html, data) { var $div $(html); if (data) { $div.data(data); } $container.append($div); return $div; }带事件绑定需求时可以顺带扩展成一个带选项的版本function appendDiv($container, options) { var $div $(options.html); if (options.className) { $div.addClass(options.className); } if (options.data) { $div.data(options.data); } $div.attr(data-id, options.id || ); $container.append($div); return $div; }如果项目里大量使用类似逻辑还可以直接挂到jQuery原型上$.fn.appendDiv function (options) { var $div $(options.html).addClass(options.className || ); if (options.data) { $div.data(options.data); } this.append($div); return $div; }; // 使用 $(#container).appendDiv({ html: div动态内容/div, className: dynamic-row, data: { order: 1 } });6.3 什么时候别封装封装的边界也要心里有数。如果一个函数只有一两处调用或者每次调用的参数千奇百怪就不要强行封装。为了一处使用写个函数反而是过度设计。我的判断标准是使用次数达到3次以上、参数模式足够稳定才值得抽出来。临时脚本、Demo演示直接三两行append写完简单明了比抽象重要。还记得上一次我在老项目里整理一批动态渲染代码就是把重复了七八遍的创建div设置class绑定data追加抽成了一个小工具函数改动需求时只改一处全页面同步生效。这种少写重复代码的收益往往比一开始想象的更大。最后分享一点个人体会每次有人问我append怎么用我第一反应不是背API而是先反问你想往哪里插、插完之后要干什么。前端的动态DOM操作把API抄一遍跑通很容易但只有真正理解了节点生命周期、事件委托和渲染性能这三件事才算是把append用明白了。建议你打开浏览器控制台自己创建几个div并append到document.body上试一试再手动改改类名、绑个事件感受一下动手一次比看十遍文档都管用。