深入浅出掌握 DOM 操作:从节点原理到事件委托实战
写 JavaScript 就别想绕开 DOM。你随便打开一个网页按 F12 在控制台敲一行document.querySelector(video)再补一句v.style.rotate -90deg整个视频立刻横过来——这种“指哪打哪”的爽感就是 DOM 操作最直观的样子。作为前端开发或者说所有跟浏览器打交道的人DOM 就是你操作页面的遥控器。这套东西不难但细节非常多很多新手写业务代码能跑一换场景就懵根本原因就是光背了 API没搞懂 DOM 到底是怎么组织的。这篇内容适合所有正在学 JavaScript 的人不管你刚看完基础语法还是已经在用框架写项目都值得回头把 DOM 这层补扎实。我会从 DOM 的底层结构讲起逐步拆到查询、修改、事件、监听、报错排查最后用一个控制台旋转视频的小例子把整个知识点串起来。里面所有代码我都自己跑过坑也替你踩好了你照着敲就行。1. 先把 DOM 这层窗户纸捅破1.1 DOM 不是 JavaScript 的专利而是浏览器的“内存地图”很多人刚开始学的时候会有个错觉DOM 是 JavaScript 的一部分。这个印象不怪你因为几乎所有教材里 DOM 都是跟着 JavaScript 一起教的网上搜“JavaScript 基础”也一定会带上 DOM。但严格来说DOM 的全称是 Document Object Model文档对象模型它是由浏览器提供的一套标准接口跟你用的浏览器、脚本语言都没绑定关系。JavaScript 只是操作 DOM 最常用的一门语言理论上任何能调用浏览器接口的脚本都能操作它。你可以把 DOM 理解成浏览器在内存里画的一张“地图”。浏览器拿到 HTML 源代码之后并不是直接往屏幕上一丢而是先把那串纯文本解析成一棵结构化的树每个标签、每段文字、每个属性都对应树上的一个节点。JavaScript 要改页面上的东西第一步就是顺着这棵树找到目标节点然后改它的属性、内容或者样式。这个过程不需要重新请求服务器页面就实时变了。理解了这一点你就知道“DOM 操作慢”这句话是怎么回事了。操作对象不是浏览器屏幕而是一棵可能包含几万个节点的树。你每改一次节点浏览器都可能触发 reflow重新计算布局或者 repaint重新绘制改得越频繁性能越受影响。框架所谓的“性能优化”本质上也绕不开减少对 DOM 的直接操作次数。1.2 从 HTML 到 DOM 树一次解析就完成的转换给你一个最简单的页面结构我们看看浏览器是怎么拆的!DOCTYPE html html head title示例页面/title /head body div idapp p classcontent你好DOM/p /div /body /html这段代码在浏览器里不是按字符串顺序平铺处理的解析器会从根节点document开始一层一层往下读遇到新标签就生成一个元素节点遇到文字就生成一个文本节点遇到属性就挂在对应元素节点上。最终形成的树大致是这样document根节点html元素节点head元素节点title元素节点“示例页面”文本节点body元素节点div#app元素节点p.content元素节点“你好DOM”文本节点注意几个关键点第一这棵树的最顶层是document不是html。你可以用document.documentElement去拿html节点但document本身是整棵树的入口。第二文本节点也是节点哪怕是换行符和空格在 DOM 树里都可能生成文本节点。第三注释节点也存在不过平时操作得少。很多新手在写“通过父元素找子元素”的循环时会遇到childNodes和children的结果对不上原因就在这childNodes返回所有子节点包括文本节点和注释节点children只返回元素节点。你要是想遍历“标签”而不是“文字”老老实实用children否则会踩到一堆看不见的空文本节点。1.3 三类节点元素、文本与属性的区分逻辑DOM 规范里节点类型有很多种但我们日常打交道的主要是三类元素节点对应 HTML 标签、文本节点对应文字内容、属性节点对应标签上的id、class、src这些。为什么要把它们区分开因为操作方式完全不一样。元素节点可以用getElementById、querySelector这些方法直接找文本节点一般得通过元素节点的textContent或innerText去读写属性节点则走setAttribute、getAttribute或直接点语法el.id xxx。还有一个容易混淆的点getAttribute(value)和el.value在表单元素里经常会读出不一样的值。前者读的是 HTML 里写死的那个属性值后者读的是元素当前的实际值。你在输入框里打了字之后再用getAttribute(value)回去读拿到的还是初始值只有el.value才是最新的。这个区别在写表单校验时就特别重要后面我会专门展开。2. 最常用的 DOM 操作查找元素、修改样式、增删节点2.1 查找元素querySelector 为什么是首选操作 DOM 的第一步永远是“先找到那个元素”。老一点的 API 有getElementById、getElementsByClassName、getElementsByTagName后来 DOM 规范加了querySelector和querySelectorAll这两个是选择器风格的方法参数就是你写 CSS 时用的那种选择器语法。我的建议是新代码统一用querySelector/querySelectorAll别混着用。原因有三个一个方法覆盖所有查找场景。不管你是按 id、class、属性还是层级关系找一个选择器就能写完不用记三套不同命名风格的方法。和 CSS 语法统一可读性好。团队里谁看代码都明白.list li.active是在找什么换成getElementsByClassName一长串反而费劲。querySelector返回的是查找结果里的第一个元素符合“精确查找用 CSS 选择器”的直觉。不过要注意querySelectorAll返回的是NodeList它是个类数组但不是一个真正的数组。你直接对它调用map、filter会报错得先转一下Array.from(document.querySelectorAll(.item))或者用展开运算符[...document.querySelectorAll(.item)]。另外提一个被问烂了的问题querySelector和getElementById到底谁快实测下来在很大很复杂的文档里getElementById确实有优势因为它是按 id 索引直接命中。但在绝大多数业务页面里这个性能差异可以忽略不计。为了几微秒的差距牺牲代码可读性不值得。真要优化重点应该放在减少查询次数上——把反复查同一个元素的结果存到变量里而不是每次用都重新查一遍。2.2 修改内容与样式innerHTML、textContent 和 style 操作找到元素之后的“改”大概分成三类改内容、改属性、改样式。改内容有两条路innerHTML和textContent。它们最大的区别是innerHTML会把字符串当作 HTML 解析插入b标签真的会加粗textContent则是把字符串当成纯文本就算里面写着b也只会给你显示原文符号。很多人图省事喜欢用innerHTML我劝你改掉这个习惯。只要你插入的内容里混进了一丁点用户输入就很容易成为 XSS 注入的入口。这个问题我在安全部分会详细说这里先记住默认用textContent除非你明确需要插入带格式的 HTML。改属性可以分成两类情况。一类是标准属性像id、href、src、value直接通过点语法修改就行img.src new.jpg。另一类是自定义属性或者叫非标准属性比如>// 等价于 el.dataset.id 123 // 等价于 el.getAttribute(data-id, 123)改动自定义属性之后用getAttribute也能读到但dataset的好处是属性名会自动把>const v document.querySelector(video); v.style.rotate -90deg;只要页面上有一个video这段代码就能让视频逆时针旋转 90 度。注意不是transform: rotate(-90deg)而是新的独立属性rotate。它和transform: rotate()的区别在于CSStransform里面如果还有其他变换函数写在一起容易互相覆盖而rotate是独立属性写起来更省心浏览器兼容性也足够新了。以后在控制台鼓捣页面玩这一招非常实用。2.3 创建节点document.createElement 的正确用法有时候你需要在页面上动态新增元素而不是修改已有的。这时候就用document.createElement创建元素节点然后通过各种插入方法把它挂到 DOM 树里。老牌的三个方法是appendChild、insertBefore和replaceChild后来新增了append、prepend、before、after等更直观的方法。还是要提醒一次childNodes和children的坑appendChild接受任意节点包括文本节点和注释节点而业务里通常只想挂元素所以只要你插入的是document.createElement创建的元素就没问题。一个高频场景是“循环创建列表”。很多人第一反应是拼接字符串然后innerHTML一次性塞进去这个写法简单但有两个问题一是性能每次改innerHTML都会让浏览器重新解析那一整块内容循环里反复操作会非常卡二是安全字符串拼接容易带上未经转义的内容为 XSS 留了后门。正确做法是循环里createElement然后appendChild。有些人担心创建几百个节点会不会慢其实在循环里反复 append 确实会影响性能所以更好的做法是用DocumentFragment先把所有节点攒起来最后一次挂到页面const list document.getElementById(list); const fragment document.createDocumentFragment(); for (let i 0; i 100; i) { const li document.createElement(li); li.textContent 第 i 项; fragment.appendChild(li); } list.appendChild(fragment);DocumentFragment是一个“隐形容器”它不属于主 DOM 树所有对它的操作都不会引发页面重绘。等全部节点都攒好了一次性把整个片段挂上去浏览器只处理一次渲染性能会好很多。这个技巧在渲染大量列表项时非常有用。2.4 删除节点removeChild 与 remove 的区别删除节点听起来最简单但里面也有坑。老写法是先找到父元素再通过父元素把子节点删掉const parent document.getElementById(list); const child document.getElementById(item-1); parent.removeChild(child);新一点的写法直接用child.remove()就行不需要再向上找父节点。这个方法兼容性已经很好可以放心用。还有个新手特别容易犯的错误写了element.remove()之后还想继续用这个元素干点别的结果发现它已经不在 DOM 树里。这只是你把它从可见 DOM 树里摘下来了并不是销毁了变量引用还活着元素本身还在内存里后面想还能重新挂回去。如果你确实想“删除”一个元素并释放内存除了remove()之外还要把原变量置为null并且确保没有其他变量引用它。另外删除和隐藏是完全两回事。隐藏用display: none或者visibility: hidden元素还在 DOM 树上还能被querySelector找到只是用户看不见。删除则意味着从结构上移除。这个区别在做单页应用时特别重要很多人把“用户点删除”做成了display: none结果页面上数据越来越多最后带来一堆隐藏节点的包袱。3. 事件监听与动态交互让页面真正“活”起来3.1 addEventListener 和 onclick 的区别以及一个常被忽略的细节给元素绑事件有两条路线。老式写法是onclick直接赋值button.onclick function() { console.log(clicked); };新写法是用addEventListenerbutton.addEventListener(click, function() { console.log(clicked); });两者最大的区别是onclick同一时间只能绑一个处理函数再绑一个会把前面的覆盖掉addEventListener可以给同一个事件绑多个处理函数按绑定顺序依次执行。还有addEventListener支持第三个参数可以指定事件在捕获阶段还是冒泡阶段处理onclick只能在冒泡阶段处理。所以从灵活性来说addEventListener是绝对的主流。但大家经常忽略一个细节addEventListener的第三个参数如果不传默认是false也就是冒泡阶段触发。你如果绑了一个点击事件又想阻止事件一路冒泡上去需要在回调里调event.stopPropagation()。但如果你用了一些组件库某些组件内部可能会在捕获阶段就把事件拦截了你的监听根本不会触发。遇到“明明绑了事件就是没反应”的情况先别怀疑选择器可以试试把第三个参数改成true在捕获阶段监听。还有一点容易被忽略匿名函数一旦绑上去就没法移除了。如果你想用removeEventListener解绑那回调函数必须是一个具名函数得先在外部定义好再传进去。3.2 事件冒泡与事件委托动态列表监听的推荐方案浏览器在事件处理上有两个阶段捕获阶段从根节点一路向内走到目标元素冒泡阶段从目标元素一路向外走回根节点。绝大多数业务代码处理的是冒泡阶段。这不是理论空谈冒泡是“事件委托”的基础。事件委托的核心思路是与其给子元素一个个绑事件不如把事件绑在它们的共同父元素上利用事件冒泡在父元素里分清到底是哪个子元素触发的。这个方案在两种场景下特别香第一种是列表项特别多一个个绑不仅代码啰嗦还消耗内存。第二种是列表项是动态生成的比如点击“加载更多”后新增了 10 个卡片如果一开始就给每个卡片绑了事件新卡片就没人管了。但事件委托绑在父元素上不管子元素后来新增多少事件都能通过冒泡到达父元素天然就支持动态内容。具体写法是这样的document.getElementById(list).addEventListener(click, function(event) { const target event.target.closest(.item); if (!target) return; console.log(点击了, target.dataset.id); });这里用closest从目标元素向上找到最近的匹配元素好处是即使你点的是列表项里的一个小图标也能正确判断出是哪一个列表项。这段代码不仅性能更好还彻底避免了“动态生成的元素没绑定事件”这个经典需求。3.3 MutationObserver监听 DOM 元素自身变化的高级 API搜索热词里单独把“DOM 监听 API”列出来了说明很多人不知道 DOM 本身也是可以被监听的。除了用户操作触发的事件有时候你想知道某个 DOM 节点是否被脚本改动了这种情况下可以用MutationObserver。比如你要监听某个区域的子元素是否新增或删除const target document.getElementById(content); const observer new MutationObserver(function(mutationsList) { for (const mutation of mutationsList) { if (mutation.type childList) { console.log(节点变化了变化类型, mutation.type); } } }); observer.observe(target, { childList: true, subtree: true });MutationObserver的配置选项有三个常用的childList监听子节点增删、attributes监听属性变化、characterData监听文本内容变化。如果你想监听元素上某个具体属性可以在observe的配置里写attributeFilter: [class, style]只关注这几个属性的变化。为什么不用老旧的MutationEvent因为MutationEvent是在 DOM 变化时同步触发的一次大量修改会触发大量事件直接把页面卡死。MutationObserver是异步的浏览器会把多次变化合并成一批回调给你处理性能完全不在一个量级。实际使用中一个很有价值的场景是监控第三方脚本有没有偷偷改你的页面。比如投放系统的广告脚本加载后可能会改掉你某个视频容器的尺寸你可以用MutationObserver监听这个容器的style变化发现异常就记录下来或者直接把错误报告给后端。不过要提醒一句MutationObserver存在循环触发的风险。回调里如果又改了同一个节点的属性它又会触发监听如果不加防抖或条件判断很容易造成死循环。我自己的习惯是在回调里用标志位控制或者直接取消观察再修改observer.disconnect(); target.setAttribute(data-fixed, true); observer.observe(target, { attributes: true });3.4 DOM 操作与表单HTML5 表单验证和 JS 提交的真实差异热搜里有个词条问“JavaScript 中表单提交和 H5 的区别”这也是一个非常现实的 DOM 问题。HTML5 基本每个浏览器都内置了一套表单验证能力你给input加上required、typeemail、minlength这些属性浏览器会在提交时自动校验。这套机制的好处是不用 JavaScript 也能实现基础校验。但很多人不知道如果表单里的提交按钮是用 JavaScript 触发的form.submit()浏览器的内置验证是不会自动执行的。你写了required但用户绕过提交按钮用 JS 去提交照样能通过。真正要触发 HTML5 验证得先调用form.checkValidity()或者form.reportValidity()前者只返回校验结果后者会把错误提示气泡显示出来。JavaScript 提交的方式有几种form.submit()会直接提交并刷新页面fetch或XMLHttpRequest提交则是在不刷新页面的前提下把数据发给后端这就是单页应用里最常见的 AJAX 提交方式。你用 AJAX 提交时HTML5 的校验完全不会自动触发必须自己手动处理const form document.getElementById(loginForm); form.addEventListener(submit, function(event) { event.preventDefault(); if (!form.checkValidity()) { form.reportValidity(); return; } // 到这里才能安全地发起 fetch 请求 });这里有个很容易踩的坑在表单提交事件里忘掉event.preventDefault()会导致页面刷新然后前端代码里的数据都被清掉了。这个“页面一闪”的现象特别经典几乎每个写前端的人都经历过。一定要记得在submit回调的第一行就把默认行为停掉。4. DOM 运行时报错排查与安全边界别在生产环境踩坑4.1 null 读取属性和 undefined is not a function前端运行时报错里最经典的一句就是Cannot read properties of null (reading xxx)。翻译成人话就是你拿着一个null当节点用了而null上没有你要的那个属性或方法。为什么会拿到null十个里面有九个是document.querySelector没找到元素。排查这个问题的顺序我一般固化成一个套路先确认选择器有没有写错。#app少了个#或者把class和id搞混了都会查不到。确认脚本执行时机。如果脚本写在head里而你要找的元素在body后面脚本执行的时候 DOM 还没构建完就找不到。这时候要么把script放到页面底部要么在DOMContentLoaded事件回调里再执行。检查目标元素是不是在 iframe 里。如果是要用 iframe 的contentDocument而不是外层页面去查找。最后再怀疑是不是元素被动态渲染了。比如使用框架时某个列表数据还没回来DOM 压根不存在。我还见过一种隐蔽情况两段代码都用了document.querySelector(.item)前面一段代码有 JS 报错中断了导致后面代码没执行。这时候没有 DOM 相关的报错反而只有一个TypeError报错很容易让人误判。所以排查任何页面上“看不见的问题”都先完整看一遍控制台的错误列表不要只盯某一条。4.2 用字符串调用函数与避免 eval搜索引擎里有人搜“JavaScript 通过字符串调用函数”这个需求常见于后端配置下发给前端告诉前端“点击这个按钮时执行openModal函数”。实现方式有两种但只有一种是安全的。第一种是在全局作用域里查找把函数挂到window上然后通过window[openModal]()调用。这个方案符合 JS 的对象模型闭包也不受影响前提是函数确实定义在全局作用域下或者被显式挂载到了windowwindow.openModal function() { console.log(open); }; const funcName openModal; if (typeof window[funcName] function) { window[funcName](); }第二种是eval但强烈不建议。eval会把字符串当成代码执行等于把解释权完整交给了调用者一旦字符串内容可控就相当于给攻击者留了一扇门。如果你只是想把一段 JSON 字符串转成对象直接用JSON.parse不要用eval。“字符串调用函数”这件事经常和另一个问题连在一起为什么有些函数没有定义在window上因为用let、const声明在模块作用域里的变量不会挂到全局对象上。所以你在模块文件里声明的函数用window[funcName]()是找不到的。解决思路是显式注册到全局window.handleAction handleAction或者用 Map 维护一个函数注册表从 Map 里按名字取。这样既不污染太多全局变量又安全可控。4.3 DOM 型 XSS 与输入校验的安全底线DOM 型 XSS 是前端安全里的常见问题原理说起来很简单JavaScript 拿到了用户输入或从 URL、存储、网络接口取到的外部数据然后把它当成 HTML 插进了页面。攻击者提供的字符串里藏了一段script或者一个带事件属性的标签页面上就多出了不该存在的代码能够窃取你的数据。最常见的危险操作就是innerHTML。比如一个评论系统直接把用户输入的评论内容塞进innerHTML攻击者发一条评论内容里写着img srcx onerror偷数据代码页面一渲染就会执行。防的办法有两个层面。第一层是输出转义。如果你想保留用户输入的纯文本就用textContent而不是innerHTML。惰性观点是“我封装一个转义函数”但最干净的方案就是不要让用户的输入走 HTML 解析器直接textContent根本不会触发转义问题。第二层是输入过滤。如果某些场景确实需要用户输入富文本那就不能只靠前端过滤了必须用成熟的富文本安全方案在后端也做一层校验。前端过滤只负责防手滑防不了恶意用户直接改请求。顺便说一句任何把用户输入拼进innerHTML、outerHTML、document.write或者eval里的代码都值得在代码审查时被打回去重写。处理用户字符串时其实还有一个容易忽略的点很多“javascript: 和 h5 表单提交区别”类的问题本质都跟“输入没有做统一处理”有关表单校验只是其中一环最终防线永远是“输出时不信任任何前端传过来的东西”。4.4 一个容易懵的问题未 new 完的对象为什么能使用 prototype这个话题看着和 DOM 关系不大但它其实是理解浏览器对象模型的一把钥匙。很多人写代码时看到Element.prototype、HTMLElement.prototype上的方法产生疑惑这个对象还没有真正构造完为什么 prototype 上的方法就能用答案在于 JavaScript 原型链的机制。构造函数执行的过程中this的原型在函数体执行之前就已经被设置好了。你用new调用构造函数时引擎会先创建一个新对象把这个对象的原型指向构造函数的prototype属性然后才执行函数体。所以在函数体内部即使你还没有执行到return对象本身的继承关系已经建立this通过原型链能找到挂在 prototype 上的方法。这就是“新对象还没构造完prototype 上的方法已经可用了”的原因。这个原理对 DOM 操作的启示是DOM 元素对象也遵循同样的原型链从元素节点逐层连接到HTMLElement.prototype、Element.prototype、Node.prototype再连接到最上层的EventTarget.prototype。你才能在任何一个元素上调用addEventListener因为这个方法定义在EventTarget.prototype上所有元素都继承自它。理解了原型链你也就知道为什么我们可以给Element.prototype扩展新方法。5. 把知识串起来一个网页视频旋转功能的小实战5.1 需求拆解与代码实现热搜里那条javascript:v document.queryselector(video);v.style.rotate -90deg其实可以扩展成一个小实战。假设你有一个在线视频教学网站视频默认横屏但你想在控制台或者浏览器书签里写一小段脚本把任意网页的视频旋转 90 度方便在竖屏显示器上观看。第一步找到页面上所有视频而不是只找第一个。因为有些页面有多个视频或者有预览图但不是真正的videoconst videos document.querySelectorAll(video); videos.forEach((v) { v.style.rotate -90deg; });第二步加上一些保护逻辑如果页面上一个视频都没有提示一下不要让代码白跑一趟if (videos.length 0) { console.log(当前页面没有找到视频标签); }第三步把这串代码封装成一个可复用的函数下次在控制台直接调用function rotateAllVideos(deg -90) { const videos document.querySelectorAll(video); if (videos.length 0) { console.warn(页面上没有 video 元素); return; } videos.forEach((v) { v.style.rotate deg deg; }); return videos.length; } rotateAllVideos(-90);最后一步提供一个恢复原状的函数。这个细节很多人会漏掉改了样式之后不知道怎么还原。由于style.rotate是行内样式直接赋空字符串就会让浏览器回到样式表默认状态function resetAllVideos() { document.querySelectorAll(video).forEach((v) { v.style.rotate ; }); }这段小工具代码把所有前文提到的知识点串起来了查找元素、遍历节点、修改样式、封装成函数。你把它保存成书签在任意网页点击书签就能自动执行。5.2 给多个视频同时操作与异常处理在真实页面上跑这段代码时会遇到很多“干净 demo”里遇不到的情况。第一个坑是video外面套了一层自定义播放器容器视频旋转了但容器没旋转导致视频旋转以后一半露在外面。解决办法是把旋转样式作用于外层容器而不是视频本身const video document.querySelector(video); const wrapper video.closest(div); // 找到最近的父容器 if (wrapper) { wrapper.style.rotate -90deg; }第二个坑是某些播放器禁止了右键但控制台操作不受影响。如果你要在控制台执行得注意页面是否对console做了篡改。有些防御手段会在页面加载时把console.log重写成空函数你调试时看不到输出。这时候直接用alert或者把调试信息展示在页面上。第三个坑是 CSP内容安全策略限制。有些站点通过 HTTP 头禁止了内联脚本运行你在地址栏输入javascript:代码可能会被浏览器拦截但是 F12 控制台里执行通常不受影响。遇到没反应的情况检查控制台是不是有 CSP 报错。第四个坑是视频本身有transform: rotate(...)的样式。style.rotate和 CSS 里的transform是两个独立属性如果你旋转之后视频没有按预期变化可能就是页面原本已经写了 transform 相关样式。可以利用开发者工具的“Styles”面板看看当前生效样式再做相应的合并处理。5.3 一个合并对象的常用写法写这种多功能小工具时经常要处理配置项就会遇到“合并两个对象”的需求。比如刚才的视频旋转工具你想支持用户传入自定义配置预设的配置和用户配置要合并。两种写法都行第一种是Object.assignconst defaultConfig { deg: -90, target: video }; const userConfig { target: .player }; const finalConfig Object.assign({}, defaultConfig, userConfig); // finalConfig { deg: -90, target: .player }第二种是展开运算符const finalConfig { ...defaultConfig, ...userConfig };这两种写法都能实现“后面的覆盖前面的”效果。它们在业务代码里极其常见尤其是在封装通用函数、处理配置项时。不过要注意它们都是浅拷贝如果配置项里有嵌套对象需要更深层的合并时要自己实现或引入工具函数。比如配置里有{ controls: { volume: 0.5 } }浅拷贝之后后传入的配置可能把整个controls覆盖掉而不是只覆盖某个字段。这个细节在写复杂配置时非常容易埋雷。5.4 如何用事件委托监听动态列表再给这个实战补一个真实需求视频网站一般会有“推荐列表”用户点“加载更多”之后新的视频卡片加进来了。如果按照老思路给每个卡片绑点击事件那新卡片不会自动有事件。这是我最常被问到的问题之一。用事件委托来解决就非常简单。在列表容器上绑一个点击事件通过closest判断用户是否点到了卡片元素const list document.getElementById(recommendList); list.addEventListener(click, function(event) { const card event.target.closest(.video-card); if (!card) return; const videoId card.dataset.id; playVideoById(videoId); });这段代码有几个好处不管列表里的卡片是初始存在的还是后来加载的只要它们都在#recommendList下面点击就能被捕获事件只绑定了一个不会因为列表数据上千导致内存储存几百个监听函数代码的意图也很清楚一看就知道这个列表的点击由谁处理。如果把动态加载也加上整个流程就完整了。真实项目里写完推荐列表的渲染函数之后事件委托代码完全不用动。这种“一次绑定永久生效”的特性是事件委托最大的价值。6. 写在最后DOM 学习的一个核心建议如果你刚把上面这些内容跟着敲了一遍你会发现 DOM 操作本质上并不复杂难的是在真实环境里遇到各种意料之外的情况。我个人的体会是不要急于背 API多打开控制台去折腾把页面上的元素当成实验对象今天旋转变换一下视频、明天给列表批量加样式后天用MutationObserver盯着某个元素看它啥时候被改。踩几次坑比看十篇教程都管用。最后再分享一个小技巧写 DOM 操作代码时时刻问自己两句话。第一句这个节点现在真的存在吗第二句这段代码执行时用户输入的内容有没有可能进入 HTML 解析流程想清楚这两个问题能避开前端开发里绝大部分常见的坑。剩下那些看不懂的报错就交给控制台和断点调试一行一行看数据到底在哪个环节变了答案总会浮出来。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →