在前端开发实践中,动态生成列表是最常见的操作之一。然而稍有不慎,就可能引发 XSS 安全漏洞或性能瓶颈问题。以下是经过验证且值得信赖的实践原则:推荐使用 createElement 搭配 textContent 来防范注入攻击,模板字符串仅作为结构骨架使用;优先采用 template 标签结合 cloneNode(true) 实现列表模板的重复利用;在高频渲染场景中,利用 DocumentFragment 进行批量 DOM 插入以优化性能;若需模拟响应式数据更新,只需封装一个 renderList 函数执行全量重绘即可满足多数需求。

模板字符串内插入变量时如何避免被解析为纯文本
直接使用 innerHTML 拼接字符串存在风险,一旦变量中包含 < 或 " 等特殊字符,不仅会破坏页面结构,还可能为 XSS 攻击打开方便之门。因此必须先进行转义处理再插入,或者直接选用更安全的替代方案。
推荐采用 document.createElement 与 textContent 的组合策略:每个字段单独通过 textContent 赋值,天然规避注入风险。如果确实需要渲染富文本内容,建议使用 DOMPurify.sanitize() 过滤后再交由 innerHTML 处理。
- 避免编写
item.innerHTML = `—— 当- ${name}
`name来自用户输入时,相当于给攻击者预留了后门 - 推荐写法:
const li = document.createElement('li'); li.textContent = name; - 模板字符串只用于搭建结构骨架,不嵌入变量:
const template = `
template 标签与 innerHTML 在批量渲染列表时该如何选择
是浏览器原生支持的“离线” DOM 容器,其中的内容不会渲染、不会执行脚本、也不会加载资源,非常适合作为可复用的列表项模板。相较于拼接字符串再通过 innerHTML 插入的方式,它更加安全可靠,尤其在需要绑定事件或保持子节点状态时优势明显。
但需要注意: 的内容必须通过 content.cloneNode(true) 进行复制,直接使用 appendChild 只会移动节点而非创建副本。
- 定义模板:
- 获取模板:
const tmpl = document.getElementById('item-template').content; - 填充并追加元素:
const el = tmpl.cloneNode(true); el.querySelector('.title').textContent = item.title; list.appendChild(el); - 务必保留
true参数,否则克隆的仅是外层节点本身
循环渲染时如何有效避免重复创建 DOM 节点带来的性能损耗
每次新增列表项时调用 createElement 或克隆 本身没有问题,但在高频操作场景中(例如滚动加载上千条数据),频繁的节点创建容易导致页面卡顿。优化关键不在于“减少创建次数”,而在于“降低重排重绘的触发频率”。
将所有新节点先存入 DocumentFragment,最后一次性 append 到目标容器中,能够显著减少 layout 回流的次数,提升渲染性能。
- 错误示例:
items.forEach(item => list.appendChild(createItem(item)));—— 每次 append 都可能触发重排 - 正确做法:
const frag = document.createDocumentFragment(); items.forEach(item => frag.appendChild(createItem(item))); list.appendChild(frag); - 如果使用
,同样先将克隆节点添加到frag中,最后再统一挂载
如何在原生 JavaScript 中模拟 Vue/React 风格的响应式列表
原生 JS 虽然缺乏响应式数据绑定机制,但可以借助 Proxy 监听数组变化,并结合模板实现自动 DOM 更新。不过需注意:push、splice 等变异方法不会自动触发 set 拦截,要么重写这些方法,要么使用 Reflect.set 并手动触发更新通知。
更实用的方案是封装一个 renderList 函数,接收数据数组和渲染函数作为参数,每次数据变更时执行全量重绘——只要列表项不超过几百个,性能完全能够满足日常需求。
- 简洁封装:
function renderList(container, data, renderItem) { container.innerHTML = ''; data.forEach(item => container.appendChild(renderItem(item))); } - 调用时传入编译好的节点:
renderList(listEl, items, item => createItem(item)); - 不建议尝试监听
Array.prototype.push——改写原型容易引发兼容性问题,且无法捕获字面量赋值操作
实际项目中最容易被忽视的是模板节点的复用边界问题。 适用于结构固定、字段较少的列表场景;一旦涉及条件分支(例如不同状态显示不同子组件)、嵌套列表或复杂交互逻辑,手写模板很快就会变得难以维护——此时应考虑引入轻量级框架或自定义元素来管理。
