武汉的开发者们注意啦!AI技术战略、框架以及最佳实战尽在Azure OpenAI Day 了解详情
写点什么

喊话 JavaScript 开发者:玩 DOM 也要专业范儿

  • 2019-10-22
  • 本文字数:6359 字

    阅读完需:约 21 分钟

喊话JavaScript开发者:玩DOM也要专业范儿

别再害怕 DOM 了,让我们充分挖掘 DOM 的潜力,你会真的爱上它。


2008 年,当我刚成为一名专业 Web 开发人员参加工作时,我了解一些 HTML、CSS 和 PHP 的知识。那时我也在学习 JavaScript,因为它可以用来显示和隐藏元素和制作下拉菜单之类很酷的事情。


当时我在一家小公司工作,我们主要为客户创建 CMS 系统。彼时我们需要一个多文件上传器,这是当时的原生 JavaScript 无法实现的功能。


经过一番搜索,我发现了一个基于 Flash 的精美解决方案,还有一个名为 MooTools 的 JavaScript 库。MooTools 有一个很酷的 $ 函数来选择 DOM 元素,并带有诸如进度条和 Ajax 请求之类的模块。几周后我发现了 jQuery,仿佛打开了新世界的大门。它不再需要冗长、笨拙的 DOM 操作,取而代之的是简单、可链接的选择器,并且还附带了一堆有用的插件。


快进到 2019 年,今天的世界由框架统治。如果你作为 Web 开发人员的事业生涯始于过去十年中,那么你很可能接触不到什么“原始”的 DOM。你甚至可能根本用不着它。


尽管诸如 Angular 和 React 之类的框架让 jQuery 的热度大减,但它仍在被 6600 万个网站所使用,这是一个惊人的数字,约占全球所有网站的 74%。


jQuery 的遗产给人留下了深刻的印象,它给标准带来的影响力有一个很好的例子,那就是模仿 jQuery 的 $ 函数的 querySelector 和 query-SelectorAll 方法。讽刺的是,这两种方法可能是 jQuery 热度下降的最大元凶,因为这两种方法替代了 jQuery 最常用的功能:轻松选择 DOM 元素。


但是原生 DOM API 很 冗长。我的意思是说,一边是 $,另一边却是 document.query-SelectorAll。这就是让开发人员抵触原生 DOM API 的原因所在。但实际上这完全没必要。


原生 DOM API 很棒,而且 非常 有用。是的,它很冗长,但这是因为它们是低级构建块,上面是要构建抽象的。而且如果你真的不想多打字的话:所有现代编辑器和 IDE 都提供出色的代码完成功能。你也可以为最常用的功能加上别名,我会在后文给出示例。


我们开始吧!

选择元素

单元素

要使用任何有效的 CSS 选择器选择单个元素,请输入:


document.querySelector(/* your selector */)
复制代码


可以使用下面的任何选择器:


document.querySelector('.foo')            // class selectordocument.querySelector('#foo')            // id selectordocument.querySelector('div')             // tag selectordocument.querySelector('[name="foo"]')    // attribute selectordocument.querySelector('div + p > span')  // you go girl!
复制代码


如果没有匹配的元素,它将返回 null。

多元素

要选择多个元素,请输入:


document.querySelectorAll('p')  // selects all <p> elements
复制代码


你可以用 document.querySelectorAll,用法与 document.querySelector 相同。任何有效的 CSS 选择器都可以,唯一的区别是 querySelector 将返回单个元素,而 querySelectorAll 将返回包含找到的元素的静态 NodeList。如果没有找到任何元素,它将返回一个空的 NodeList。


NodeList 是一个可迭代的对象,它 类似 数组,但 实际上 不是数组,因此它没有相同的方法。你可以在其上运行 forEach,但不能用 map、reduce 或 find。


如果确实需要在其上运行数组方法,则可以使用解构或 Array.from 将其转换为数组:


const arr = [...document.querySelectorAll('p')];orconst arr = Array.from(document.querySelectorAll('p'));arr.find(element => {...});  // .find() now works
复制代码


querySelectorAll 方法与诸如 getElements-ByTagName 和 getElementsByClassName 之类的方法的不同之处在于,这些方法返回的是 实时 收集的 HTMLCollection,而 query-SelectorAll 返回的是 静态 的 NodeList。


因此,如果执行 getElementsByTagName(‘p’), 一个p将从文档中删除,也会从返回的 HTMLCollection 中删除。


但如果执行 querySelectorAll(‘p’),一个p将从文档中删除,但它仍将存在于返回的 NodeList 中。


另一个重要的区别是,HTMLCollection 只能包含 HTMLElement,而 NodeList 可以包含任何类型的 Node。

相对搜索

你不一定需要在 document 上运行 query-Selector(All)。你可以在任何 HTML-Element 上运行它以执行相对搜索(relative search):


const div = document.querySelector('#container');div.querySelectorAll('p')  // finds all <p> tags in #container only
复制代码


但这仍然很冗长!


如果你还是觉得打的字太多了,则可以为两种方法起别名:


const $ = document.querySelector.bind(document);$('#container');const $$ = document.querySelectorAll.bind(document);$$('p');
复制代码


搞定。

爬一爬 DOM 树

使用 CSS 选择器选择 DOM 元素意味着我们只能沿着 DOM 树向 移动。没有 CSS 选择器可以沿树向上选择父项。


但是我们可以使用 closest() 方法向 DOM 树上面移动,该方法也能用任何有效的 CSS 选择器:


document.querySelector('p').closest('div');
复制代码


这将找到 document.querySelector(‘p’) 选择的段落中最接近的父div元素。你可以将这些调用链接起来,往树上多爬几层:


document.querySelector('p').closest('div').closest('.content');
复制代码

添加元素

向 DOM 树添加一个或多个元素的代码很容易变得冗长拖沓,因而臭名昭著。假设你要在页面上添加以下链接:


<a href="/home" class="active">Home</a>
复制代码


你需要:


const link = document.createElement('a');link.setAttribute('href', '/home');link.className = 'active';link.textContent = 'Home';document.body.appendChild(link);
复制代码


现在想象一下,这套操作要在 10 个元素上重复十次……


至少 jQuery 允许你执行以下操作:


$('body').append('<a href="/home" class="active">Home</a>');
复制代码


其实原生也有等效操作,想不到吧?


document.body.insertAdjacentHTML('beforeend','<a href="/home" class="active">Home</a>');
复制代码


使用 insertAdjacentHTML 方法,你可以在第一个参数指示的四个位置向 DOM 中插入任意有效的 HTML 字符串:- ‘beforebegin’:元素之前。


  • ‘afterbegin’:在元素的第一个子元素之前。

  • ‘beforeend’:在元素的最后一个子元素之后。

  • ‘afterend’:元素之后。


<!-- beforebegin --><p>  <!-- afterbegin -->  foo  <!-- beforeend --></p><!-- afterend -->
复制代码


这样就能更容易地指定插入新元素的确切位置。假设你要在这个p之前插入a,如果没有 insertAdjacentHTML,则必须执行以下操作:


const link = document.createElement('a');const p = document.querySelector('p');p.parentNode.insertBefore(link, p);
复制代码


现在我们只要:


const p = document.querySelector('p');p.insertAdjacentHTML('beforebegin', '<a></a>');
复制代码


这也是插入 DOM 元素的一种原生等效方法:


const link = document.createElement('a');const p = document.querySelector('p');p.insertAdjacentElement('beforebegin', link);
复制代码


还有文本:


p.insertAdjacentText('afterbegin', 'foo');
复制代码

移动元素

insertAdjacentElement 方法也可以用来在同一文档中的现有元素之间移动。当使用 insertAdjacentElement 插入的元素已经成为文档的一部分时,它就会被移动。


如果你有以下 HTML:


<div class="first">  <h1>Title</h1></div><div class="second">  <h2>Subtitle</h2></div>
复制代码


h1之后插入h2


const h1 = document.querySelector('h1');const h2 = document.querySelector('h2');h1.insertAdjacentElement('afterend', h2);
复制代码


它将被简单地 移动而不是复制


<div class="first">  <h1>Title</h1>  <h2>Subtitle</h2></div><div class="second">
</div>
复制代码

替换元素

一个 DOM 元素可以使用其 replaceWith 方法替换为其他任意 DOM 元素:


someElement.replaceWith(otherElement);
复制代码


替换它的元素可以是使用 document.create-Element 创建的新元素,也可以是已经属于同一文档的元素(在这种情况下,它还是会被移动而不是复制):


<div class="first">  <h1>Title</h1></div><div class="second">  <h2>Subtitle</h2></div>const h1 = document.querySelector('h1');const h2 = document.querySelector('h2');h1.replaceWith(h2);// result:<div class="first">  <h2>Subtitle</h2></div><div class="second">
</div>
复制代码

移除元素

只需调用其 remove 方法:


const container = document.querySelector('#container');container.remove();  // hasta la vista, baby
复制代码


比老方法好得多:


const container = document.querySelector('#container');container.parentNode.removeChild(container);
复制代码

从原始 HTML 创建元素

insertAdjacentHTML 方法允许我们将原始 HTML 插入文档中,但是如果我们想从原始 HTML 中 创建 元素并在以后使用它又该怎么办?


为此,我们可以使用 DomParser 对象及其方法 parseFromString。DomParser 提供了将 HTML 或 XML 源代码解析为 DOM 文档的功能。我们使用 parseFromString 方法创建一个仅包含一个元素的文档,并仅返回这一个元素:


const createElement = domString => new DOMParser().parseFromString(domString, 'text/html').body.firstChild;const a = createElement('<a href="/home" class="active">Home</a>');
复制代码

检查 DOM

标准 DOM API 还提供了一些方便的方法来检查 DOM。例如,matchs 确定一个元素是否将与某个选择器匹配:


<p class="foo">Hello world</p>const p = document.querySelector('p');p.matches('p');     // truep.matches('.foo');  // truep.matches('.bar');  // false, does not have class "bar"
复制代码


你还可以使用 contains 方法检查一个元素是否是另一个元素的子元素:


<div class="container">  <h1 class="title">Foo</h1></div><h2 class="subtitle">Bar</h2>const container = document.querySelector('.container');const h1 = document.querySelector('h1');const h2 = document.querySelector('h2');container.contains(h1);  // truecontainer.contains(h2);  // false
复制代码


你可以使用 compareDocumentPosition 方法获得有关元素的更多详细信息。使用此方法,你可以确定一个元素是在另一个元素之前还是之后,或者其中一个元素是否包含另一个元素。它返回一个整数,该整数表示对比的元素之间的关系。


下面这个示例与上一个示例的元素相同:


<div class="container">  <h1 class="title">Foo</h1></div><h2 class="subtitle">Bar</h2>const container = document.querySelector('.container');const h1 = document.querySelector('h1');const h2 = document.querySelector('h2');//  20: h1 is contained by container and follows containercontainer.compareDocumentPosition(h1);// 10: container contains h1 and precedes ith1.compareDocumentPosition(container);// 4: h2 follows h1h1.compareDocumentPosition(h2);// 2: h1 precedes h2h2.compareDocumentPosition(h1);
复制代码


从 compareDocumentPosition 返回的值是一个整数,其数值表示相对于参数(为此方法提供)的 node 之间的关系。


因此,考虑语法 node.compareDocumentPo-stion(otherNode),返回值的含义是:- 1:node 不在同一文档中。


  • 2:otherNode 在 node 之前。

  • 4:otherNode 在 node 之后。

  • 8:otherNode 包含 node。

  • 16:otherNode 由 node 包含。


可能会设置多个数值,这就是为什么在上面的示例中 container.compareDocumenPosition(h1) 会返回 20——因为 h1 包含在 container 中,所以你可能以为会返回 16。但是 h1 也跟随 container(4),因此结果值为 16 + 4 = 20。

请多说一点!

你可以通过 MutationObserver 接口观察对任何 DOM 节点的更改。这包括文本更改,将节点添加到被观察的节点,或从观察的节点中删除节点或更改节点的属性。


MutationObserver 是一个功能强大的 API,几乎可以观察到 DOM 元素及其子节点上发生的任何更改。


使用回调函数调用其构造函数来创建新的 MutationObserver。每当观察到的节点发生更改时,将调用此回调:


const observer = new MutationObserver(callback);
复制代码


要观察元素,我们需要调用观察者的 observe 方法,将要观察的节点作为第一个参数,将带有选项的对象作为第二个参数。


const target = document.querySelector('#container');const observer = new MutationObserver(callback);observer.observe(target, options);
复制代码


调用 observe 后才开始观察目标。


此选项对象使用以下键:- attributes:设置为 true 时,将监视节点属性的更改。


  • attributeFilter:要监视的属性名称的数组,当 attribute 为 true 且未设置时,将监视节点的 所有 属性的更改。

  • attributeOldValue:设置为 true 时,只要发生更改,就会记录该属性的先前值。

  • characterData:设置为 true 时,它将记录文本节点中文本的更改,因此这仅适用于 Text 节点,不适用于 HTMLElement。为此,要观察的节点必须是 Text 节点,或者,如果观察者监视 HTMLElement,则需要将 option subtree 设置为 true,以监视子节点的更改。

  • characterDataOldValue:设置为 true 时,每当发生更改时,将记录特征数据的先前值。

  • subtree:设置为 true 还可以观察到所观察元素的子节点的更改。

  • childList:设置为 true 以监视元素的添加和删除子节点动作。当 subtree 设置为 true 时,还将监视子元素中子节点的添加和删除。


当调用 observe 开始观察一个元素时,将通过一个 MutationRecord 对象数组来调用传递给 MutationObserver 构造函数的回调,这些对象描述发生的更改,并描述作为第二个参数调用的观察者。


MutationRecord 包含以下属性:


  • type:更改的类型,可以是 attribute、characterData 或 childList。

  • target:已更改的元素,可以是属性、字符数据或子元素。

  • addNodes:已添加节点的列表;如果未添加,则为空的 NodeList。

  • removeNodes:已删除节点的列表;如果未删除任何节点,则为空 NodeList。

  • attributeName:更改后的属性名称;如果未更改任何属性,则为 null。

  • previousSibling:添加或删除的节点的上一个同级,或者为 null。

  • nextSibling:添加或删除的节点的下一个同级,或者为 null。


假设我们要观察属性和子节点的变化:


const target = document.querySelector('#container');const callback = (mutations, observer) => {  mutations.forEach(mutation => {    switch (mutation.type) {      case 'attributes':        // the name of the changed attribute is in        // mutation.attributeName        // and its old value is in mutation.oldValue        // the current value can be retrieved with        // target.getAttribute(mutation.attributeName)        break;      case 'childList':        // any added nodes are in mutation.addedNodes        // any removed nodes are in mutation.removedNodes        break;    }  });};const observer = new MutationObserver(callback);observer.observe(target, {  attributes: true,  attributeFilter: ['foo'], // only observe attribute 'foo'  attributeOldValue: true,  childList: true});
复制代码


观察完目标后,你可以断开观察器的连接,并在需要时调用其 takeRecords 方法以获取尚未传递给回调的任何挂起的突变:


const mutations = observer.takeRecords();callback(mutations);observer.disconnect();
复制代码

不要害怕 DOM

DOM API 是一个非常强大且用途广泛的 API,尽管它很冗长。请记住,它旨在为开发人员提供底层的构建块,以便在其上构建抽象,因此从某种意义上讲,它必须很冗长,以提供明确而清晰的 API。多打几行代码不应该阻止你充分挖掘它的潜力。


DOM 是每位 JavaScript 开发人员 必不可少的知识,因为你可能每天都在使用它。不要惧怕它,要充分利用它。然后你将成为更优秀的开发人员。


原文链接


https://itnext.io/using-the-dom-like-a-pro-163a6c552eba


2019-10-22 17:502981
用户头像
王文婧 InfoQ编辑

发布了 126 篇内容, 共 70.3 次阅读, 收获喜欢 274 次。

关注

评论 1 条评论

发布
用户头像
先问是不是害怕!初生牛犊连虎都不怕,还怕学习与操作DOM嘛。比如就性能来说,一个中大型项目里,别以为直接操作DOM就代表着快,一些初生牛犊的各种花式DOM操作会让你有惊喜的,加班review?减少直接操作DOM的现代框架或库们较大程度上抹平了水平和经验的差距。
2019-10-23 18:12
回复
没有更多了
发现更多内容

开启中文智能之旅:探秘超乎想象的 Llama2-Chinese 大模型世界

汀丶人工智能

人工智能 自然语言处理 llama 大语言模型 llama2

用友BIP,助力装备制造企业数智化转型

用友BIP

制造 数智化

九章云极DataCanvas公司入选《AIGC产业链北京专精特新企业图谱》

九章云极DataCanvas

WiFi Signal for Mac(wifi管理神器) 4.4.10永久激活版

mac

苹果mac Windows软件 WiFi Signal WiFi信号强度监测工具

ChatGPT 是如何产生心智的? | 京东云技术团队

京东科技开发者

人工智能 机器学习 ChatGPT 企业号10月PK榜

关于征集人工智能一体机系列标准参编单位的通知

中国信通院AI Infra工作组

利用ChatGPT提升测试工作效率——测试工程师的新利器(一) | 京东云技术团队

京东科技开发者

人工智能 测试 企业号10月PK榜

基于Effect的组件设计 | 京东云技术团队

京东科技开发者

前端 React Hooks 企业号10月PK榜 effect

英特尔锐炫家族迎新成员:锐炫A580兼顾价格与性能的全新选择

E科讯

软件测试/测试开发丨为什么接口自动化测试是提升职业技能的关键

测试人

Python 程序员 软件测试 接口测试 接口自动化

如何导出带有材质的GLB模型?

3D建模设计

glb 材质 纹理 贴图

用友深度参编!《煤炭行业信息技术应用创新发展报告(2023)》重磅发布

用友BIP

信创

LAS Spark 在 TPC-DS 的优化揭秘

字节跳动数据平台

数据库 大数据 数据安全 数据研发 企业号10月PK榜

私密离线聊天新体验!llama-gpt聊天机器人:极速、安全、搭载Llama 2

汀丶人工智能

人工智能 自然语言处理 nlp llama 大语言模型

发行版兴趣小组季度动态:Anolis OS 支持大热 AI 软件栈,引入社区合作安全修复流程

OpenAnolis小助手

AI 操作系统 CVE 龙蜥社区 发行版

OpenJDK17-JVM源码阅读-ZGC-并发标记 | 京东物流技术团队

京东科技开发者

ZGC 并发标记 企业号10月PK榜 JVM源码

SRE实战:如何低成本推进风险治理?稳定性与架构优化的3个策略

TakinTalks稳定性社区

离职原因千万不要这样说!

王磊

Java

关于征集中国人工智能产业发展联盟“人工智能基础平台(AI Infra)工作组”首批成员单位的通知

中国信通院AI Infra工作组

中国水泥行业数字化采购:驱动产业链供应链现代化的关键

用友BIP

数智采购 水泥行业

Mac上常用的视频编辑软件DaVinci Resolve Studio 18

展初云

Mac软件 视频编辑软件 达芬奇18

Databend join reorder 策略

Databend

ToB企业的财务核算利器——智能收款认领中心!

用友BIP

智能收款

侧发光透明LED显示屏特点优势

Dylan

商业 类型 LED显示屏 户外LED显示屏

Spring Boot 项目中 Bean 注入的方式介绍

Apifox

Java Spring Boot annotation bean Spring Boot bean

办公必备Microsoft 365 for Mac(原Office 365)

展初云

Office Mac软件

南京水务:通过推进全面预算、财务共享等数智化转型,探寻业财融合

用友BIP

业财融合

喊话JavaScript开发者:玩DOM也要专业范儿_大前端_Danny Moerkerke_InfoQ精选文章