尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

获取div中的span元素:原生DOM与Vue/jQuery场景全解析

获取div中的span元素:原生DOM与Vue/jQuery场景全解析 前端开发里“获取div中的span元素”这件事听起来简单得像刚学HTML时的课后练习可我在实际项目里见过太多在这上面翻车的人。有的拿到的是空集合一脸懵有的拿到的还是上一轮渲染留下的旧元素怎么查都是上次的状态还有的在Vue组件里写了一大堆document.getElementById却不知道有ref这回事。说白了span只是最常见的HTML标签但“获取元素”这个动作背后牵扯到原生DOM API、CSS选择器语义、动态渲染时机、框架数据绑定等一系列问题。这篇文章就围绕这一件小事把常用的写法、他们之间的差异、常见坑和调试方法串一遍。适合刚接触DOM操作的前端新人也适合写了一阵子业务但没系统梳理过这些API的开发者。1. 原生方法四种写法先分清谁是“活的”谁是“快照”1.1 按标签名硬取getElementsByTagName 的动态陷阱在原生JavaScript里最传统的写法是const container document.getElementById(container); const spans container.getElementsByTagName(span); console.log(spans.length);这段代码会把container内所有后代span元素找出来不管嵌套多深只要是span标签就算。它返回的不是数组而是一个HTMLCollection这个集合最大的特点是动态的。“动态”意味着什么意思是只要DOM发生了变化这个集合的内容会自动更新。比如你拿到spans的时候页面上有3个span之后往container里又插入了1个span再访问spans.length它已经是4了。这个特性在某些场景下很省事但在遍历的同时修改DOM就容易出大问题。最常见的例子const spans container.getElementsByTagName(span); for (let i 0; i spans.length; i) { if (spans[i].textContent remove me) { spans[i].parentNode.removeChild(spans[i]); } }如果你真的这么写会发现有些该删的span没删掉。因为每删一个spans集合的长度就变小索引也跟着变循环走着走着就跳过了某些元素。我一开始也踩过这个坑后来养成了习惯只要是拿HTMLCollection做遍历先转成数组再操作。Array.from(spans).forEach(span { // 可以安全删除或修改 });1.2 更推荐的选择器querySelector 和 querySelectorAll如果你问我现在实际项目里用哪个我基本都会说querySelector和querySelectorAll。它们允许你用完整的CSS选择器语法来定位元素可读性和扩展性都更好。// 取 container 内第一个 span const firstSpan container.querySelector(span); // 取 container 内所有 span const allSpans container.querySelectorAll(span); // 取 container 内带有 classitem 的 span const itemSpans container.querySelectorAll(span.item); // 取 container 内直接子元素中的 span const directChildSpans container.querySelectorAll(:scope span);这里有个新手容易犯的错querySelector和querySelectorAll接受的是CSS选择器所以空格不是装饰而是“后代”的意思。#container span和#container span是完全不同的两回事。前者匹配所有后代元素中的span后者只匹配container的直接子元素span。querySelectorAll返回的是NodeList在现在的浏览器里NodeList支持forEach也可以被for...of遍历用起来比HTMLCollection顺手得多。还需要注意querySelectorAll返回的是一个静态快照你查询完之后即使页面上的DOM再变化这个NodeList也不会自动同步。这在单页应用里是好事也是坏事好事是循环不会因为DOM变化而出错坏事是如果你一直持有这个集合它不会自动变成最新状态。所以动态列表变化频繁时最好在需要的时候重新查询而不是把查询结果缓存到全局变量里。1.3 直接子元素还是所有后代children / childNodes 的边界有时候需求不是“找到div里的所有span”而是“找到div的直接span子元素”。这时候除了用选择器还可以用children属性。const container document.getElementById(container); const directChildren container.children; // 所有元素子节点children返回的是元素节点的集合不会包含文本节点、注释节点。与之相对的childNodes返回的是所有子节点包括文本和注释。div idcontainer 这里有文本 spanA/span !-- 注释 -- spanB/span /div这段HTML里container.childNodes的长度可能是5文本、span、注释、span、可能还有换行带来的文本节点而container.children的长度是2。如果你只是想找span元素用children再过滤就行Array.from(container.children).filter(el el.tagName SPAN);不过大多数情况下container.querySelectorAll(:scope span)更直接。这里多说一句querySelectorAll在普通元素上使用:scope表示以这个元素自身为锚点配合就能实现“直接子元素”的效果。如果不用:scope你直接写container.querySelectorAll( span)是无效的因为:scope就是用来表示“当前调用的元素”。2. 什么都没拿到先查三个地方2.1 脚本位置和 DOMContentLoaded最常见的空集合原因一个非常常见的问题代码明明没写错可执行的时候就拿不到元素。你第一反应是怀疑选择器但很多时候问题出在脚本执行时机上。看这个例子!DOCTYPE html html head script const container document.getElementById(container); const spans container.querySelectorAll(span); console.log(spans.length); // 0甚至报错 /script /head body div idcontainer span1/span span2/span /div /body /html浏览器解析HTML是自上而下的当head里的脚本执行时body里的div都还没被解析到getElementById(container)得到的只能是null再往下就直接报错了。解决办法有三种把脚本挪到/body之前让DOM先解析完给script加上defer属性让脚本在DOM解析完成后执行监听DOMContentLoaded事件在回调里再操作DOM。其中defer是现在比较推荐的方式它不会阻塞页面解析又保证了脚本在DOM就绪后执行。如果你写的是模块脚本typemodule它默认就会延迟执行所以不太需要担心这个问题。2.2 动态渲染Vue 里数据没到DOM 里自然没有很多前端新人从静态页面转到Vue或React之后会带着原来的习惯直接操作DOM。比如在Vue组件的mounted里写mounted() { const spans document.querySelectorAll(#container span); console.log(spans.length); }如果这个#container是靠接口数据渲染出来的v-for循环的数据还没返回那么mounted执行时DOM里根本没有对应的span。这不是你选择器的问题而是渲染时机的问题。在Vue里数据更新之后DOM的变更不会立刻生效往往要等nextTick。async mounted() { this.list await fetchList(); await this.$nextTick(); const spans document.querySelectorAll(#container span); console.log(spans.length); }当然如果你完全通过数据驱动来展示根本不需要去查DOM。但在某些必须要操作真实DOM的场景比如封装第三方图表库、手动聚焦、读取元素尺寸nextTick就是你的好朋友。2.3 静态集合和动态集合长期缓存查询结果会出问题这一节我在前面已经提到但值得单独拎出来说因为它真的太容易踩坑了。getElementsByTagName、getElementsByClassName返回的是HTMLCollection是动态的会跟着DOM变化自动增删元素。querySelectorAll返回的是NodeList在目前实现里是静态的拿到手之后不会再变。这两种行为差异在实际业务中影响很大。比如你在一个长列表页面里写了一个搜索框用户输入关键词之后列表里的span会被重新渲染。如果你在初始化时保存了一次querySelectorAll的结果那么搜索之后这个集合还是旧的里面的DOM元素可能已经不在页面上了。这时候如果你还去读它的textContent拿到的可能是“幽灵数据”。我的习惯是用完就扔随时需要随时查。除非有明确的性能优化需求不要长期持有查询结果。如果必须监听DOM变化来更新引用用MutationObserver。const observer new MutationObserver(() { updateSpanList(container.querySelectorAll(span)); }); observer.observe(container, { childList: true, subtree: true });这样每次DOM结构变化后都会重新查询一次保证引用里面永远是最新的span列表。3. jQuery 老项目里的写法选择器语义不能凭感觉3.1 后代选择器和子元素选择器一个空格引发的血案虽然现在新项目基本不用jQuery了但老项目维护、开源插件二次开发时你还是会频繁遇到。jQuery里获取span主要靠$函数和选择器// 所有后代 span $(#container span); // 直接子元素 span $(#container span);这两段代码的区别和原生CSS选择器完全一致但实际开发中见过好几个人把漏掉然后发现拿到的元素比预期多。记住一句话空格是“后代”大于号是“儿子”。另外jQuery里还经常用.find()和.children()$(#container).find(span); // 等价于 $(#container span) $(#container).children(span); // 等价于 $(#container span).find()是在所有后代里找.children()只找一层子元素。如果你在写插件不想依赖具体选择器结构用这两者组合会更清晰。3.2 first 和 :first-child看着像实际不是一回事热词里有个“jquery 第一个子元素”很多新手会写成$(#container span:first-child)然后发现有时候好用有时候不好用。原因很简单$(#container span)匹配到的是多个span的集合.first()是取这个集合里的第一个属于集合操作:first-child是CSS伪类它匹配的条件是“这个元素必须是它父元素的第一个子节点”。看这段HTMLdiv idcontainer p段落/p span第一个 span/span span第二个 span/span /div$(#container span:first-child)返回的是空因为这个span不是container的第一个子元素第一个是p。而$(#container span).first()返回span第一个 span/span。所以想取集合里的第一个用.first()想取每个父元素下的第一个span子元素用:first-child。两个需求别搞混。3.3 获取 jQuery 集合中的原生 DOM 元素还有一个很常见的困惑$(#container span)返回的到底是个什么东西它是一个jQuery对象内部封装了匹配到的DOM元素数组。你可以像数组一样用索引去取原生DOM元素const spans $(#container span); const firstNativeSpan spans[0]; console.log(firstNativeSpan.textContent);也可以用.get(index)const firstNativeSpan spans.get(0);但要注意spans[0]是原生DOM元素没有jQuery的.text()、.css()等方法想用jQuery方法处理单个元素可以.eq(index)spans.eq(0).css(color, red);这里eq返回的还是jQuery对象可以继续链式调用。搞清楚这三者的区别老项目维护能少踩很多坑。4. Vue 场景能不用 DOM 查找就不用非要用就 ref4.1 给元素挂 ref然后直接读 $refs在Vue里我见过最“暴力”的写法是直接在组件里写document.getElementById。不能说不能用但一旦组件被复用、组件位置变化全局ID冲突的风险就来了。Vue本身提供了ref用途之一就是让你在组件内部安全地拿到某个DOM元素。模板里template div refcontainer span内容/span /div /templateOptions API里this.$refs.container.querySelector(span);Composition APIscript setup里import { ref, onMounted } from vue; const container ref(null); onMounted(() { const span container.value.querySelector(span); });注意container变量名要和模板里的refcontainer保持一致。ref绑定在普通DOM元素上对应的值就是该DOM元素绑定在子组件上对应的就是子组件实例。4.2 v-for 循环里的 ref拿到的不一定是数组如果你在v-for里给span也加了refdiv idlist span v-for(item, index) in items :refspan index :keyindex {{ item }} /span /div在this.$refs.span0就能拿到对应的span元素。如果用的是同一个ref名字比如:refspanItem那this.$refs.spanItem会是一个数组里面是按渲染顺序排列的DOM元素。这里有个细节this.$refs.spanItem的顺序通常和items数组顺序一致但如果你对列表做了过滤、排序或者有v-if参与控制顺序不一定可靠。最稳妥的方式还是在v-for里用:key来标识并通过:ref生成带索引的名字。还有一个常见的问题是v-for渲染的数据是异步获取的在mounted里访问this.$refs.span0可能是undefined。因为此时数据还没到列表还没渲染出来。需要在拿到数据后使用await this.$nextTick()或者直接监听数据变化后再访问。4.3 事件处理时用 $event 获取当前 span更快更稳为了获取一个span去全局查找其实绕了远路。如果你只是想在点击某个span时拿到它直接在模板里绑定事件然后在回调里取$event.currentTarget这是更符合框架习惯的做法。template div span clickhandleClick点我/span /div /templatefunction handleClick(event) { const span event.currentTarget; // 这里的 span 就是绑定了 click 的元素 console.log(span.textContent); }这里为什么用currentTarget而不是targetcurrentTarget是当前事件监听器所绑定的元素也就是那个span而target是事件真正发生的源元素。如果span里面还嵌套了em或者b点击到子元素时target会是子元素currentTarget仍然是span。你如果非要用target记得加closestconst span event.target.closest(span);这样即使点击的是span内部的文本节点或嵌套标签也能找到对应的span。4.4 别混了HTML 的 span 和 OpenTelemetry 的 Span 是两码事搜索“span元素”的时候偶尔会看到“OpenTelemetry span”之类的词混进来。它们的名字一样但完全不是一个领域的东西。HTML里的span是行内元素用来包裹文本、给局部内容做样式OpenTelemetry里的Span是链路追踪中的一个基本单元代表一个操作片段包含名称、开始时间、结束时间、属性等。前端代码里如果引入了opentelemetry/api你可能会写span.end()来结束一个追踪段这和操作DOM里的span标签没有任何关系。所以如果你在帖子或者搜索结果里看到“span操作”这几个字先确认上下文是DOM还是监控。搞混了会写出很奇怪代码比如把document.querySelector(span)拿出来的元素当成追踪对象去调用.end()控制台直接报错。5. 进阶场景iframe、Shadow DOM 和 XPath 查法5.1 iframe 里的 span先拿 contentDocument如果div不在当前页面文档而在一个iframe内部用普通的querySelector是拿不到的。得先获取iframe的document。const iframe document.getElementById(myIframe); const iframeDoc iframe.contentDocument; const spans iframeDoc.querySelectorAll(div span);这里有个硬性限制跨域情况下浏览器会因为同源策略阻止你访问contentDocument得到null。你的页面和iframe里的页面如果不在同一个域名、协议、端口下除非iframe允许跨域访问比如通过postMessage通信否则拿不到里面的元素。实际操作中跨域iframe内部的DOM操作基本做不了只能在iframe内部页面里执行脚本再通过postMessage把结果传出来。5.2 Shadow DOM 里的 span普通选择器穿透不了在使用Web Component或一些封装组件库时元素可能被放进Shadow DOM里。比如一个自定义组件的内部结构my-widget #shadow-root (open) div spanshadow span/span /div /my-widget此时在页面顶层执行document.querySelector(my-widget span)是找不到的因为普通CSS选择器不会穿透shadow边界。你需要先拿到shadow host元素再访问它的shadowRootconst host document.querySelector(my-widget); const shadowRoot host.shadowRoot; const span shadowRoot.querySelector(span);注意shadowRoot只有在mode: open模式下才可以通过外部访问到。如果组件创建时用了closed模式host.shadowRoot会是null那就真的拿不到了。所以遇到“明明页面上有span就是查不到”的情况看看是不是在shadow DOM里。5.3 XPath 和 TreeWalker非主流但偶尔好用如果CSS选择器写得很痛苦或者你想按轴关系比如“找当前div后面所有span”来获取可以试试XPath。const result document.evaluate( //div//span, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null ); for (let i 0; i result.snapshotLength; i) { console.log(result.snapshotItem(i)); }这段代码会返回整个文档里所有div里的span。XPath的//div//span对应CSS的div span//div/span对应div span。XPath很强大但日常业务代码里用它反而增加维护成本更多出现在爬虫或自动化测试脚本里。你只需要知道有这个方法真遇到复杂定位时能想到它就行。还有一个偏底层的是TreeWalker它可以在DOM树上自由行走比如遍历所有元素节点判断tagName SPAN再收集。性能和灵活性都还行但代码量不小一般场景不值得。6. 调试与避坑控制台技巧和一份问题速查表6.1 控制台三步定位法我遇到“span获取不到”的问题时不会干瞪眼直接在浏览器控制台按顺序查先在Elements面板里手动选中那个div容器然后在控制台输入$0回车。$0代表当前在Elements面板中选中的元素这个特性在Chrome和Edge里都支持。输入$0.querySelectorAll(span)看看命中了什么。如果返回空说明div下面确实没有span或者span在更深的嵌套里再考虑是不是shadow DOM/iframe的问题。如果选择器很复杂拆开测试。比如先查$0.querySelectorAll(div)看看有多少div再多一层层往下查利用控制台补全提示确认类名、id拼写。控制台里还有快捷函数$$(div span)等价于document.querySelectorAll(div span)少打很多字。$$在Elements面板和Console面板都能用不过它是浏览器控制台提供的高级功能不要在页面代码里依赖它。6.2 常见问题速查表下面这份表格是我自己整理出来的碰到问题可以直接对着查现象可能原因解决办法container是null当前DOM中没有对应id或脚本执行过早检查id拼写把脚本放到DOM就绪后用DOMContentLoadedquerySelectorAll返回空的NodeList选择器层级不对或者元素在v-if未渲染区域检查CSS选择器空格和用nextTick等待渲染getElementsByTagName遍历时漏删元素HTMLCollection动态更新索引变化先把集合转成数组再遍历能查到一个span但修改后页面没变化可能操作的是静态快照不是最新DOM确认是不是querySelectorAll的结果被缓存重新查询iframe内部查不到跨域或被contentDocument安全策略限制在iframe内部执行脚本通过postMessage传数据shadow DOM里的span查不到普通选择器无法穿透shadow边界先拿shadowRoot再查内部$(#container span:first-child)取到的不是第一个span伪类:first-child要求元素是父元素的第一个子节点改用.first()或:first-of-type6.3 三个独家避坑经验第一个经验是关于“元素集合的生命周期”。在单页应用里列表数据一变DOM元素可能全部重建。如果你在页面初始化时用一个全局变量保存了所有span的引用等数据更新后这些引用可能指向已经不在文档里的“孤儿节点”。这不是API的bug而是静态快照和动态页面的天然矛盾。我的做法是尽量让DOM查询发生在事件回调或者异步数据到达之后而不是组件初始化时更不要长期缓存查询结果。第二个经验是“事件委托比给每个span绑定事件更稳”。如果你要为一组span添加点击事件别一个个去绑定spans.forEach(span { span.addEventListener(click, handler); });更好的做法是把监听器挂在容器上利用事件冒泡container.addEventListener(click, (e) { const span e.target.closest(span); if (span container.contains(span)) { handler(span); } });这样做的好处是以后新增的span不用再单独绑定事件了只要它们在容器内就能自动被处理。第三个经验是关于选择器性能的。我以前也纠结过getElementsByTagName比querySelectorAll快多少。实测下来在几百个元素的页面里性能差异微乎其微。真正影响性能的反而是频繁的全局查询和布局抖动。所以我的建议是优先写querySelectorAll代码可读性更好只有在明确了热点路径、确实需要极致优化时才去考虑用getElementsByTagName这种底层集合。大多数业务瓶颈不在这种毫秒级差异上。7. 写在最后方法论比 API 细节更重要如果你完整看到这里应该已经发现“获取div中的span元素”这个需求本身不难难的是在不同场景里选择合适的获取方式并且搞清楚返回结果到底是动态还是静态、是当前页面还是iframe、是普通DOM还是shadow DOM。我个人在实际项目中的体会是原生API里querySelectorAll是默认选项jQuery老代码里记住“后代、子元素、.first()和:first-child”的区别Vue里尽量用ref和事件参数少用全局查找。遇到拿不到元素的问题不要急着怀疑代码先按顺序查三件事DOM渲染出来了吗选择器语义对吗元素是不是在同一个文档树里这三步能解决我遇到的90%问题。剩下的10%多半是时序问题和旧缓存引用耐心断点调试一下都会有答案。
返回列表