This website requires JavaScript.

如何优雅地操作DOM

by  shirmy  

在以前,操作DOM是一件非常麻烦的事情,虽然现在已经有类似React、Vue、Angular等框架帮助我们更容易地构建界面。但是我们仍然有必要学习原生DOM的操作方式来扩展我们的知识面,并且可以来应对一些不使用框架的场景,经过长时间的发展,现在的DOM API也变得更加优雅简洁了。

元素选择

单个元素

// 返回一个 HTMLElement
document.querySelector(selectors)

它提供类似jQuery的$()选择器方法,非常方便,我们可以这样使用它:

document.querySelector('.class-name') // 根据 class 选择
document.querySelector('#id') // 根据 id 选择   
document.querySelector('div') // 根据 标签 选择
document.querySelector('[data-test="input"]') // 根据属性来选择
document.querySelector('div + p > span')  // 多重选择器

多个元素

// 返回一个 NodeList
document.querySelectorAll('li') // 选择所有标签为 <li> 的元素

如果要使用Array的数组方法,需要先转成普通数组,可以这样做:

// 使用扩展运算符
const arr = [...document.querySelectorAll('li')]

// 使用 Array.from 方法
const arr = Array.from(document.querySelectorAll('li'))

但是它和getElementsByTagNamegetElementsByClassName是有区别的,getElementsByTagNamegetElementsByClassName返回的是一个HTMLCollection,它是动态的,比如当我们移除掉document中被选取的某个li标签,所返回的HTMLCollection中相应的li标签也会被移除,它具有实时性

querySelectorAll是静态的,移除document文档流中被选取的某个li标签,不会影响返回的NodeList,它没有实时性

HTMLCollection 和 NodeLis 的异同

  • HTMLCollection是元素的集合(只包含元素)
  • NodeList是文档节点的集合(包含元素也包含其它节点)
  • HTMLCollection动态集合,节点变化会反映到返回的集合中
  • NodeList静态集合,节点的变化不会影响返回的集合
  • HTMLCollection实例对象可以通过idname属性引用节点元素
  • NodeList只能使用数字索引引用

选择范围

我们可以限制选择的范围,而不至于每次都在document上进行选择,可以这样做:

// 只获取 #container 下的所有 li 标签
const container = document.querySelector('#container')
container.querySelectorAll('li')

进一步封装

我们可以封装成类似jQuery的写法,用$进行选择:

const $ = document.querySelector.bind(document)
$('#container')

const $$ = document.querySelectorAll.bind(document)
$$('li')

这里注意,我们需要使用bindthis的指向绑定到document上,否则直接把函数赋值给变量获取到的是一个普通函数,会导致this指向window

向上选择DOM

我们还可以获取某个Element的最近父元素,通过使用closest方法

// 获取距离 li 标签最近的上级 div 标签
document.querySelector('li').closest('div')

// 再更上一层,获取最近的上级名为 content 的元素
document.querySelector('li').closest('div').('.content')

添加元素

这里假设我们要添加这样一个元素

<a href="/home" class="active">Home</a>

在过去,我们需要这样来添加元素

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

在有了jQuery后,我们可以这样来添加元素

// 一句就能搞定
$('body').append('<a href="/home" class="active">Home</a>')

现在,我们可以借助insertAdjacentHTML来实现类似jQuery的方法

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

这里需要传入两个参数,第一个参数是插入的位置,第二参数是插入的HTML片段,位置可选参数如下:

  • beforebegin 插入某个元素之前
  • afterbegin 插入到第一个子元素之前
  • beforeend 插入到最后一个子元素之后
  • afterend 插入到元素之后
<!-- beforebegin -->
<div>
  <!-- afterbegin -->
  content
  <!-- beforeend -->
</div>
<!-- afterend -->

通过这个API,可以更方便地指定插入位置。假如要把a标签插入到div之前,我们以前需要这样做:

const link = document.createElement('a')
const div = document.querySelector('div')
div.parentNode.insertBefore(link, div)

而现在直接指定位置就可以了

const div = document.querySelector('div')
div.insertAdjacentHTML('beforebegin', '<a></a>')

还有两个相似的方法,但第二个元素传入的不是HTML字符串,而是传一个元素或文本

const link = document.createElement('a')
const div = document.querySelector('div')
// 插入元素
div.insertAdjacentElement('beforebegin', link)

插入文本

// 插入文本
div.insertAdjacentText('afterbegin', 'content')

移动元素

上面介绍的insertAdjacentElement也可以移动文档流上的一个元素,假如有这样的HTML片段:

<div class="first">
  <h1>Title</h1>
</div>
<div class="second">
  <h2>Subtitle</h2>
</div>

我们需要把h2标签插入到h1标签下面

// 分别获取两个元素
const h1 = document.querySelector('h1')
const h2 = document.querySelector('h2')

// 指定把 h2 插入到 h1 下面
h1.insertAdjacentElement('afterend', h2)

注意,这是移动,而非拷贝,此时的HTML变成:

<div class="first">
  <h1>Title</h1>
  <h2>Subtitle</h2>
</div>
<div class="second">
  <!-- h2 标签被移动了  -->
</div>

元素替换

我们可以直接使用replaceWith方法,通过这个方法,可以创建一个元素来进行替换,也可以选择一个已有元素进行替换,后者会移动被选择的元素,而非拷贝。

someElement.replaceWith(otherElement)
<!-- 替换前 -->
<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')

// 用 h2 替换掉 h1
h1.replaceWith(h2)
<!-- 替换后 -->
<div class="first">
  <!-- h1 被 h2 替换 -->
  <h2>Subtitle</h2>
</div>
<div class="second">
  <!-- h2 被移动 -->
</div>

移除一个元素

只需要调用remove()方法就可以了

const container = document.querySelector('#container')
container.remove()
// 以前的移除方法
const container = document.querySelector('#container')
container.parentNode.removeChild(container)

使用原生HTML片段创建元素

从上面可以了解到insertAdjacentHTML方法可以帮助我们插入HTML字符串到指定的位置,假如我们要先创建元素,而不是需要立即插入。

这时就需要借助DomParser对象,它可以解析HTMLXML来创建一个DOM元素,它提供了parseFromString方法进行创建并返回解析后的元素。

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

元素匹配

matches

matches可以帮助我们判断某个元素是否和选择器相匹配。

<p class="foo">Hello world</p>
const p = document.querySelector('p')
p.matches('p')     // true
p.matches('.foo')  // true
p.matches('.bar')  // false, 不存在 class 名为 bar

contains

也可以使用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)  // true
container.contains(h2)  // false

compareDocumentPosition

使用node.compareDocumentPosition(otherNode)方法可以帮助我们确定某个元素的确切位置,它会返回数字来指示位置,返回值的意思如下,如果满足多个条件,会返回相加值:

  • 1: 比较的元素不在同一个document
  • 2: otherNodenode之前
  • 4: otherNodenode之后
  • 8: otherNode包裹node
  • 16: otherNodenode包裹
<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 被 container 所包裹,并且在 container 之后 16 + 4 = 20
container.compareDocumentPosition(h1) 
// 10: container 包裹 h1,并且在 h1 之前 8 + 2 = 10
h1.compareDocumentPosition(container)
// 4: h2 在 h1 的后面
h1.compareDocumentPosition(h2)
// 2: h1 在 h2 的前面
h2.compareDocumentPosition(h1)

MutationObserver

我们还可以使用MutationObserver来监听DOM树的变动

// 当监听到元素的变动就会调动 callback 方法
const observer = new MutationObserver(callback)

然后我们需要使用observer方法监听某个node的变化,否则不会监听,它接收两个参数,第一个参数是监听目标,第二个参数是监听选项。

const target = document.querySelector('#container')
const observer = new MutationObserver(callback)
observer.observe(target, options)

当发生变化时,就会调用callback方法,此时,我们就可以在callback中监听变化,并监听callbackmutations类型进行相应地处理:

具体的配置及其含义可以参考文档MutationObserver

// step1: 获取元素
const target = document.querySelector('#container')

// step2: 编写回调函数,处理逻辑
const callback = (mutations, observer) => {
  mutations.forEach(mutation => {
    switch (mutation.type) {
      case 'attributes':
        // 通过 mutation.attribute 获取改变的 attribute
        // 通过 mutation.oldValue 获取旧值
        break
      case 'childList':
        // 通过 mutation.addedNodes 获取添加的节点
        // 通过 mutation.removedNodes 获取移除的节点
        break
    }
  })
}

// step3: 传入 callback 并实例化
const observer = new MutationObserver(callback)

// step4: 开始监听并根据需求设置监听选项
observer.observe(target, {
  attributes: true, // 监听 attribute 变化
  attributeFilter: ['foo'], // 只监听属性包含 foo,需要先把 attribute 设置为 true
  attributeOldValue: true,  // 发生改变时,记录 attribute 之前的值
  childList: true // 监听元素的添加和删除
})

当完成监听时,可以通过observer.disconnect()方法来中止监听,并且可以在之前通过takeRecords()来处理未传递的MutationRecord

const mutations = observer.takeRecords()
callback(mutations)
observer.disconnect()

小结

通过上述这些强大的API,可以非常方便地对 DOM 进行操作,满足各种不同的需求,此外,还有一些没有介绍到的,比如IntersectionObserver可以监听目标元素和文档视窗的交叉状态来实现图片懒加载。所以,在使用框架进行开发时,我们也需要深入理解 DOM,这样才可以对整个 DOM 结构有更清晰的认识,更好地发挥它们的潜力,优雅地实现各种效果。

参考文档:

相关推荐
  • 自己写一个Webpack插件
  • Vue双向绑定原理及实现
  • Koa2框架原理及实现
  • 使用Jest对Vue进行自动化测试
  • Jest前端自动化测试框架
  • 使用Nginx配置HTTPS和反向代理
  • 全栈开发—Travis CI持续集成
  • 全栈开发—博客服务端(Koa2)
  • 全栈开发—博客后台管理系统
  • 全栈开发—博客前端展示(Nuxt.js)
评论
  • 剑豪
    剑豪 剑豪

    优秀啊,老哥

  • marvin
    marvin marvin

    11

    @张:

    文章写得不错

  • 张
    张

    文章写得不错

  • 孔
    孔

    我看看能不能排到第一名哈!