系列文章目录《JavaScript 基础与进阶笔记》前期偏基础巩固与常见面试点后续进入闭包、异步、工程化等进阶主题第 01 篇数据类型与类型判断第 02 篇变量声明与作用域第 03 篇闭包与高阶函数第 04 篇函数工厂第 05 篇this 指向与绑定第 06 篇原型与原型链第 07 篇类与继承第 08 篇JS 执行机制与异步队列第 09 篇数组常用方法第 10 篇字符串算法第 11 篇常见手写题合集上第 12 篇常见手写题合集下第 13 篇Promise 与 async/await第 14 篇数据结构基础第 15 篇垃圾回收与内存第 16 篇DOM 基础全面解析第 17 篇DOM 性能与渲染第 18 篇DOM 交互补充第 19 篇DOM 实战案例本文文章目录系列文章目录前言一、无限滚动IntersectionObserver 哨兵1.1 思路1.2 最小实现1.3 性能注意二、图片懒加载与预览2.1 懒加载2.2 点击预览事件委托三、拖拽排序HTML5 Drag and Drop 简版四、富文本渲染与安全4.1 安全 vs 不安全4.2 XSS 类型口述4.3 防御清单五、案例与前几篇对照六、易混淆点归纳七、思考与练习总结前言第 1618 篇已覆盖 DOM API、渲染性能与事件/可见性本篇用几个典型场景把知识串起来不求面面俱到。选取无限滚动IO 哨兵、图片懒加载与预览、简易拖拽排序、富文本渲染与安全。每个案例点出性能要点与常见坑超大数据量列表的虚拟滚动见第 12 篇XSS 体系化防御见系列后续安全专题。一、无限滚动IntersectionObserver 哨兵1.1 思路在列表底部放一个哨兵元素高度 1px 即可当其进入视口时请求下一页并追加 DOM加载完成后继续观察哨兵。[ 已渲染列表项 ... ] [ #sentinel 哨兵 ] ← 进入视口 → fetch 下一页1.2 最小实现constlistdocument.querySelector(#list);constsentineldocument.querySelector(#sentinel);letpage1;letloadingfalse;lethasMoretrue;constloadMoreasync(){if(loading||!hasMore)return;loadingtrue;try{constrowsawaitfetchPage(page);// 模拟接口if(rows.length0){hasMorefalse;observer.disconnect();return;}constfragdocument.createDocumentFragment();rows.forEach((text){constlidocument.createElement(li);li.textContenttext;frag.appendChild(li);});list.appendChild(frag);page;}finally{loadingfalse;}};constobservernewIntersectionObserver((entries){if(entries.some((e)e.isIntersecting))loadMore();},{rootMargin:200px});observer.observe(sentinel);asyncfunctionfetchPage(p){awaitnewPromise((r)setTimeout(r,300));returnp3?[p${p}-1,p${p}-2,p${p}-3]:[];}1.3 性能注意点说明防抖加载loading标志避免重复请求批量插入DocumentFragment减少回流第 17 篇数据极大改虚拟列表第 12 篇勿无限堆 DOM解绑无更多数据disconnect()路由离开unobserve二、图片懒加载与预览2.1 懒加载简单场景原生loadinglazy。imgsrcphoto.jpgloadinglazyalt描述/需提前加载距离、或统一 IO 管理与第 18 篇一致constionewIntersectionObserver((entries,obs){entries.forEach((entry){if(!entry.isIntersecting)return;constimgentry.target;img.srcimg.dataset.src;img.removeAttribute(data-src);obs.unobserve(img);});},{rootMargin:100px});document.querySelectorAll(img[data-src]).forEach((img)io.observe(img));占位建议固定宽高或 aspect-ratio避免加载后CLS布局偏移。2.2 点击预览事件委托相册缩略图用委托绑在容器上点击后把大图塞进模态层constgallerydocument.querySelector(#gallery);constmodaldocument.querySelector(#preview);constbigmodal.querySelector(img);gallery.addEventListener(click,(e){constthumbe.target.closest(img[data-full]);if(!thumb)return;big.srcthumb.dataset.full;big.altthumb.alt;modal.hiddenfalse;});modal.addEventListener(click,(){modal.hiddentrue;big.removeAttribute(src);// 可选释放大图});安全data-full应来自可信 CDN 地址勿把用户输入直接当 URL用户上传图需服务端校验。三、拖拽排序HTML5 Drag and Drop 简版适合有序列表重排移动端常改用手势库此处展示桌面端最小思路。ulidsortablelidraggabletrueA/lilidraggabletrueB/lilidraggabletrueC/li/ulconstuldocument.querySelector(#sortable);letdragElnull;ul.addEventListener(dragstart,(e){constlie.target.closest(li);if(!li)return;dragElli;e.dataTransfer.effectAllowedmove;});ul.addEventListener(dragover,(e){e.preventDefault();// 允许 dropconstlie.target.closest(li);if(!li||lidragEl)return;constrectli.getBoundingClientRect();constaftere.clientYrect.toprect.height/2;ul.insertBefore(dragEl,after?li.nextSibling:li);});ul.addEventListener(dragend,(){dragElnull;// 可把 ul 内顺序同步到后端});注意dragover里preventDefault必须否则无法 drop。频繁insertBefore会触发回流项不多时可接受项多考虑只记录索引、drop 时再 DOM 一次。自定义拖拽样式可用setDragImage动画优先transform第 17 篇。四、富文本渲染与安全评论、文章详情常需渲染HTML 富文本。核心原则永远不要把不可信字符串直接innerHTML进页面。4.1 安全 vs 不安全// ❌ 用户输入含 script 或 onerror → XSSbox.innerHTMLuserComment;// ✅ 纯文本展示box.textContentuserComment;// ✅ 必须渲染 HTML白名单净化后再插入box.innerHTMLDOMPurify.sanitize(userComment);4.2 XSS 类型口述类型来源例存储型服务端存了恶意 HTML评论区持久化脚本反射型URL 参数回显到页?qscript...DOM 型前端把不可信数据写进 DOMinnerHTML location.hash4.3 防御清单输出转义默认textContent必须 HTML 时用DOMPurify等白名单库。CSPContent-Security-Policy限制脚本来源后续安全篇展开。Cookie敏感 Token 设HttpOnly降低被脚本读走风险。富文本编辑器提交前服务端再次净化前端净化不能替代后端。// 简易 HTML 转义无标签场景constescapeHtml(s)s.replace(//g,amp;).replace(//g,lt;).replace(//g,gt;).replace(//g,quot;);面试聚焦富文本 白名单净化 服务端校验innerHTML userInput是典型反例。五、案例与前几篇对照案例用到的前序知识无限滚动第 18 篇 IO第 17 篇 Fragment第 12 篇虚拟列表升级懒加载/预览第 18 篇 IO /loadinglazy第 18 篇事件委托拖拽排序第 16 篇 DOM 增删第 17 篇回流富文本第 16 篇textContentvsinnerHTML六、易混淆点归纳无限滚动 ≠ 虚拟列表前者可不断 append数据量大必须虚拟化。IO 哨兵记得loading锁与disconnect。预览模态大图 URL 须可信防javascript: 伪协议等。拖拽在 touch 端体验差移动场景另选方案。前端 DOMPurify不能代替后端过滤。七、思考与练习1.无限滚动列表越来越长滚动变卡优先怎么优化解析上虚拟列表第 12 篇控制 DOM 数量而非一味加页。2.评论接口返回 HTML前端如何渲染解析DOMPurify.sanitize后再innerHTML服务端仍须白名单过滤。3.相册 100 张图每个缩略图单独click监听好吗解析不好容器事件委托closest(img)第 18 篇。4.拖拽排序时为何dragover要preventDefault解析默认不允许 drop不阻止则drop不触发。5.哨兵刚进视口就连续触发两次加载可能原因解析未加loading锁或未在请求期间unobserve哨兵。总结无限滚动底部IO 哨兵 分页请求 Fragment追加数据量大改虚拟列表。图片简单loadinglazy要控距用IO预览用委托 模态。拖拽HTML5 DnD 最小实现注意回流与移动端差异。富文本禁裸innerHTML净化 服务端防 XSS。DOM 阶段至此完结。下一篇进入CSS 布局居中、BFC、Flex/Grid 等。
DOM 实战案例:无限滚动、懒加载与富文本安全
发布时间:2026/5/27 4:09:18
系列文章目录《JavaScript 基础与进阶笔记》前期偏基础巩固与常见面试点后续进入闭包、异步、工程化等进阶主题第 01 篇数据类型与类型判断第 02 篇变量声明与作用域第 03 篇闭包与高阶函数第 04 篇函数工厂第 05 篇this 指向与绑定第 06 篇原型与原型链第 07 篇类与继承第 08 篇JS 执行机制与异步队列第 09 篇数组常用方法第 10 篇字符串算法第 11 篇常见手写题合集上第 12 篇常见手写题合集下第 13 篇Promise 与 async/await第 14 篇数据结构基础第 15 篇垃圾回收与内存第 16 篇DOM 基础全面解析第 17 篇DOM 性能与渲染第 18 篇DOM 交互补充第 19 篇DOM 实战案例本文文章目录系列文章目录前言一、无限滚动IntersectionObserver 哨兵1.1 思路1.2 最小实现1.3 性能注意二、图片懒加载与预览2.1 懒加载2.2 点击预览事件委托三、拖拽排序HTML5 Drag and Drop 简版四、富文本渲染与安全4.1 安全 vs 不安全4.2 XSS 类型口述4.3 防御清单五、案例与前几篇对照六、易混淆点归纳七、思考与练习总结前言第 1618 篇已覆盖 DOM API、渲染性能与事件/可见性本篇用几个典型场景把知识串起来不求面面俱到。选取无限滚动IO 哨兵、图片懒加载与预览、简易拖拽排序、富文本渲染与安全。每个案例点出性能要点与常见坑超大数据量列表的虚拟滚动见第 12 篇XSS 体系化防御见系列后续安全专题。一、无限滚动IntersectionObserver 哨兵1.1 思路在列表底部放一个哨兵元素高度 1px 即可当其进入视口时请求下一页并追加 DOM加载完成后继续观察哨兵。[ 已渲染列表项 ... ] [ #sentinel 哨兵 ] ← 进入视口 → fetch 下一页1.2 最小实现constlistdocument.querySelector(#list);constsentineldocument.querySelector(#sentinel);letpage1;letloadingfalse;lethasMoretrue;constloadMoreasync(){if(loading||!hasMore)return;loadingtrue;try{constrowsawaitfetchPage(page);// 模拟接口if(rows.length0){hasMorefalse;observer.disconnect();return;}constfragdocument.createDocumentFragment();rows.forEach((text){constlidocument.createElement(li);li.textContenttext;frag.appendChild(li);});list.appendChild(frag);page;}finally{loadingfalse;}};constobservernewIntersectionObserver((entries){if(entries.some((e)e.isIntersecting))loadMore();},{rootMargin:200px});observer.observe(sentinel);asyncfunctionfetchPage(p){awaitnewPromise((r)setTimeout(r,300));returnp3?[p${p}-1,p${p}-2,p${p}-3]:[];}1.3 性能注意点说明防抖加载loading标志避免重复请求批量插入DocumentFragment减少回流第 17 篇数据极大改虚拟列表第 12 篇勿无限堆 DOM解绑无更多数据disconnect()路由离开unobserve二、图片懒加载与预览2.1 懒加载简单场景原生loadinglazy。imgsrcphoto.jpgloadinglazyalt描述/需提前加载距离、或统一 IO 管理与第 18 篇一致constionewIntersectionObserver((entries,obs){entries.forEach((entry){if(!entry.isIntersecting)return;constimgentry.target;img.srcimg.dataset.src;img.removeAttribute(data-src);obs.unobserve(img);});},{rootMargin:100px});document.querySelectorAll(img[data-src]).forEach((img)io.observe(img));占位建议固定宽高或 aspect-ratio避免加载后CLS布局偏移。2.2 点击预览事件委托相册缩略图用委托绑在容器上点击后把大图塞进模态层constgallerydocument.querySelector(#gallery);constmodaldocument.querySelector(#preview);constbigmodal.querySelector(img);gallery.addEventListener(click,(e){constthumbe.target.closest(img[data-full]);if(!thumb)return;big.srcthumb.dataset.full;big.altthumb.alt;modal.hiddenfalse;});modal.addEventListener(click,(){modal.hiddentrue;big.removeAttribute(src);// 可选释放大图});安全data-full应来自可信 CDN 地址勿把用户输入直接当 URL用户上传图需服务端校验。三、拖拽排序HTML5 Drag and Drop 简版适合有序列表重排移动端常改用手势库此处展示桌面端最小思路。ulidsortablelidraggabletrueA/lilidraggabletrueB/lilidraggabletrueC/li/ulconstuldocument.querySelector(#sortable);letdragElnull;ul.addEventListener(dragstart,(e){constlie.target.closest(li);if(!li)return;dragElli;e.dataTransfer.effectAllowedmove;});ul.addEventListener(dragover,(e){e.preventDefault();// 允许 dropconstlie.target.closest(li);if(!li||lidragEl)return;constrectli.getBoundingClientRect();constaftere.clientYrect.toprect.height/2;ul.insertBefore(dragEl,after?li.nextSibling:li);});ul.addEventListener(dragend,(){dragElnull;// 可把 ul 内顺序同步到后端});注意dragover里preventDefault必须否则无法 drop。频繁insertBefore会触发回流项不多时可接受项多考虑只记录索引、drop 时再 DOM 一次。自定义拖拽样式可用setDragImage动画优先transform第 17 篇。四、富文本渲染与安全评论、文章详情常需渲染HTML 富文本。核心原则永远不要把不可信字符串直接innerHTML进页面。4.1 安全 vs 不安全// ❌ 用户输入含 script 或 onerror → XSSbox.innerHTMLuserComment;// ✅ 纯文本展示box.textContentuserComment;// ✅ 必须渲染 HTML白名单净化后再插入box.innerHTMLDOMPurify.sanitize(userComment);4.2 XSS 类型口述类型来源例存储型服务端存了恶意 HTML评论区持久化脚本反射型URL 参数回显到页?qscript...DOM 型前端把不可信数据写进 DOMinnerHTML location.hash4.3 防御清单输出转义默认textContent必须 HTML 时用DOMPurify等白名单库。CSPContent-Security-Policy限制脚本来源后续安全篇展开。Cookie敏感 Token 设HttpOnly降低被脚本读走风险。富文本编辑器提交前服务端再次净化前端净化不能替代后端。// 简易 HTML 转义无标签场景constescapeHtml(s)s.replace(//g,amp;).replace(//g,lt;).replace(//g,gt;).replace(//g,quot;);面试聚焦富文本 白名单净化 服务端校验innerHTML userInput是典型反例。五、案例与前几篇对照案例用到的前序知识无限滚动第 18 篇 IO第 17 篇 Fragment第 12 篇虚拟列表升级懒加载/预览第 18 篇 IO /loadinglazy第 18 篇事件委托拖拽排序第 16 篇 DOM 增删第 17 篇回流富文本第 16 篇textContentvsinnerHTML六、易混淆点归纳无限滚动 ≠ 虚拟列表前者可不断 append数据量大必须虚拟化。IO 哨兵记得loading锁与disconnect。预览模态大图 URL 须可信防javascript: 伪协议等。拖拽在 touch 端体验差移动场景另选方案。前端 DOMPurify不能代替后端过滤。七、思考与练习1.无限滚动列表越来越长滚动变卡优先怎么优化解析上虚拟列表第 12 篇控制 DOM 数量而非一味加页。2.评论接口返回 HTML前端如何渲染解析DOMPurify.sanitize后再innerHTML服务端仍须白名单过滤。3.相册 100 张图每个缩略图单独click监听好吗解析不好容器事件委托closest(img)第 18 篇。4.拖拽排序时为何dragover要preventDefault解析默认不允许 drop不阻止则drop不触发。5.哨兵刚进视口就连续触发两次加载可能原因解析未加loading锁或未在请求期间unobserve哨兵。总结无限滚动底部IO 哨兵 分页请求 Fragment追加数据量大改虚拟列表。图片简单loadinglazy要控距用IO预览用委托 模态。拖拽HTML5 DnD 最小实现注意回流与移动端差异。富文本禁裸innerHTML净化 服务端防 XSS。DOM 阶段至此完结。下一篇进入CSS 布局居中、BFC、Flex/Grid 等。