当前位置: 首页 > 工具软件 > Vanilla JS > 使用案例 >

Vanilla JavaScript中基本的DOM操作(一)

通鸿风
2023-12-01

无论何时,我们需要进行DOM操作,我们将很快地借助于jQuery。然而,JavaScript DOM API事实上是非常有能力的,并且自从低于 IE11 被 完全放弃 后,它现在可以被放心地使用了。

在本文中,我将演示如何完成一些最常见的DOM操作任务用普通的JavaScript,即:

  • 第一节:查询和修改DOM

  • 第二节:修改类和属性

  • 第三节:监听事件

  • 第四节:动画

我将给你展示如何创建你自己的超精简的DOM库,你可以把它放入任何项目中。一路上,你将学习到,使用vanilla JS进行DOM操作不是什么高端玩法,事实上许多jQuery方法在本地API中都有直接的类似实现。

让我们开始吧……


第一节:DOM操作:查询DOM


请注意:我不会解释vanilla DOM API的全部细节,只是简单地了解一下。在示例中,你可能会遇到我没有明确介绍的方法。这时,只需要去参考优秀的 Mozilla Developer Network (MDN) 里的细节了。

DOM可以使用 .querySelector() 方法来查询,该方法把任意的CSS选择器作为一个参数:

const myElement = document.querySelector('#foo > div.bar');

这将返回第一个匹配的DOM(深度优先)。相反地,我们可以检测一个元素是否匹配一个选择器:

myElement.matches('div.bar') === true

如果我们想获取所有的匹配到的DOM,我们可以使用:

const myElements = document.querySelectorAll('.bar');

如果我们已经有了一个父元素的引用,我们可以只查询该父元素的子元素而不是整个 document。通过像这样缩小上下文,我们可以简化选择器并且提高性能。

const myChildElement = myElements.querySelector('input[type="submit"]');

// 而不是
// document.querySelector('#foo > div.bar input[type="submit"]')

那么为什么要使用其它不太方便的方法,比如 .getElementsByTagName() ?一个重要的不同就是 .querySelector() 的结果不是动态的,所以当我们动态地通过搭配一个选择器来添加一个元素(请在第三节寻找细节)时,集合将不会更新。

const elements1 = document.querySelectorAll('div');
const elements2 = document.getElementsByTagName('div');
const newElement = document.createElement('div');
document.body.appendChild(newElement);
elements1.length === elements2.length // false

另一个注意的地方是这样一个动态集合不需要预先提供所有的信息,而 .querySelectorAll() 立即收集静态列表中的所有内容,使其性能降低

使用节点列表

现在关于 .querySelectorAll() 有两个常见的陷阱。第一个就是我们不能在结果上调用节点(Node)方法并将它们传播给它的元素(就像你可能从jQuery对象中使用那样)。相反,我们必须明确地重复这些元素。另一个陷阱是:返回值是一个节点列表(NodeList),不是一个数组。这意味着常规的数组方法不能直接使用。有几个对应的NodeList实现,例如.forEach,但是任何 IE 仍然不支持。所以我们不得不首先把这和列表转换为一个数组,或者从数组原型中“借用”这些方法。

// 使用 Array.from()
Array.from(myElements).forEach(doSomethingWithEachElement);

// ES6之前的做法
Array.prototype.forEach.call(myElements, doSomethingWithEachElement);

// 简写:
[].forEach.call(myElements, doSomethingWithEachElement);

每个元素还有一些引用“家族”的默认的只读属性,所有这些都是动态的:

myElement.children
myELement.firstElementChild
myElement.lastElementChild
myElement.previousElementSibling
myElement.nextElementSibling

由于元素(Element)接口继承自节点(Node)接口,因此还可以使用以下属性:

myElement.childNodes
myElement.firstChild
myElement.lastChild
myElement.previousSibling
myElement.nextSibling
myElement.parentNode
myElement.parentElement

如果前者是唯一的参考元素,后者(除了 .parentElement )可以是任何类型的节点,例如,文本节点。我们可以检测一个给定节点的类型,像这样:

myElement.firstChild.nodeType === 3 
// 它是一个文本节点 

与任何对象一样,我们可以使用 instanceof 操作符来检查节点呃原型链:

myElement.firstChild.nodeType instanceof Text

第二节:修改类和属性


修改元素的类是这样容易:

myElement.classList.add('foo');
myElement.classList.remove('bar');
myElement.classList.toggle('baz');

你可以阅读一个更深入的讨论,关于如何用这种 Yaphi Berhanu 的快速技巧来修改类。元素属性可以像任何其他对象属性一样被访问。

// 获取一个属性值
const value = myElement.value;

// 给一个元素设置一个属性
myElement.value = "foo";

// 使用 Object.assign() 来设置多个属性
Object.assign(myElement, {
    value: 'foo',
    id: 'bar'}
);

// 移除一个属性
myElement.value = null;

注意,还有 .getAttribute().setAttribute().removeAttribute() 这些方法。这些方法可以直接修改HTML的元素属性(而不是DOM属性),这会引起浏览器重绘(你可以通过使用浏览器的 dev tools 来检查元素观察这些更改)。这种浏览器重绘不仅比设置DOM属性更加耗费性能,而且这些方法也会产生意想不到的结果

作为一个经验之谈,只能将它们用于没有相应DOM属性的属性(如 colspan),如果你真的想将这些改变“坚持”用到HTML中(例如,在克隆元素或修改它的父元素的 .innerHTML 时保留它们 - 请参阅第三部分)

添加CSS样式

CSS规则可以像其他属性一样应用;注意,这些属性在JavaScript中是驼峰式的:

myElement.style.marginLeft = '2em';

如果我们想要某些值,我们可以通过 .style 属性获得这些值。然而,这只会给我们已经明确应用的样式。为了获得计算值,我们可以使用 .window.getComputedStyle 。它接受元素并返回一个 CSSStyleDeclaration ,其中包含元素本身以及从父元素继承的所有样式:

window.getComputedStyle(myElement).getPropertyValue('margin-left')
修改DOM

我们可以像这样移动元素:

// 给 element1 追加最后一个子元素 element2
element1.appendChild(element2)

// 在element3 之间插入element2 作为 element1 的子元素
element1.insertBefore(element2, element3)

如果我们不想移动元素,而是插入一个副本,我们可以像这样克隆它:

// 新建一个克隆
const myElementClone = myElement.cloneNode()
myParentElement.appendChild(myElementClone)

.cloneNode() 方法可以选择一个布尔值作为参数;如果设置为true,则会创建一个深层副本,意味着它的子项也被克隆了。

当然,我们也可以创建全新的元素或文本节点:

const myNewElement = document.createElement('div');
const myNewTextNode = document.createTextNode('some text');

我们可以按照上面那样插入。如果我们想要移除一个元素,不能直接这样做,但我们可以从父元素中移除子元素,像这样:

myParentElement.removeChild(myElement)

这给了我们一个很好的解决方法,意思是通过引用它的父元素,可以间接地移除一个元素:

myElement.parentNode.removeChild(myElement)
元素属性

每个元素也有 .innerHTML.textContent 属性(.innerText.textContent 类似,但有一些重要的区别)。 这些分别包含HTML和纯文本内容。它们是可写属性,这意味着我们可以直接修改元素及其内容:

// 替换内部的 HTML
myElement.innerHTML = `
  <div>
    <h2>New content</h2>
    <p>beep boop beep boop</p>
  </div>
`

// 移除所有的子节点
myElement.innerHTML = null

// 添加内部的 HTML
myElement.innerHTML += `
  <a href="foo.html">continue reading...</a>
  <hr/>
`

如上所示向HTML添加标记通常是一个坏主意,因为我们将失去之前在受影响的元素上进行的任何属性更改(除非我们将这些更改作为HTML属性持续保存,如第二节中所示)和绑定事件侦听器。设置 .innerHTML 对于完全抛弃标记并将其替换为其他内容是很好的,例如,服务器呈现的标记。所以追加元素最好像这样完成:

const link = document.createElement('a')
const text = document.createTextNode('continue reading...')
const hr = document.createElement('hr')

link.href = 'foo.html'
link.appendChild(text)
myElement.appendChild(link)
myElement.appendChild(hr)

然而,使用这种方法,我们会导致两次浏览器抽回 — 每个附加元素一次 — 而更改 .innerHTML 只会导致一次。作为解决此性能问题的一种方法,我们可以首先在 DocumentFragment 中组装所有节点,然后再附加单个片段:

const fragment = document.createDocumentFragment()

fragment.appendChild(text)
fragment.appendChild(hr)
myElement.appendChild(fragment)

未完,请接第二部分


  • 参考资料

原文地址

 类似资料: