vanilla
本文包含在我们的选集Modern JavaScript中 。 如果您希望所有内容都集中在一个地方,以适应最新JavaScript,请注册SitePoint Premium并下载一个副本。
每当我们需要执行DOM操作时,我们都可以很快找到jQuery。 但是,原始的JavaScrpt DOM API本身具有相当强大的功能,并且由于IE <11已被正式放弃 ,因此现在可以毫无顾虑地使用它。
在本文中,我将演示如何使用纯JavaScript完成一些最常见的DOM操作任务,即:
- 查询和修改DOM,
- 修改类和属性,
- 听事件,以及
- 动画。
最后,我将向您展示如何创建自己的超薄DOM库,您可以将其放入任何项目中。 在此过程中,您将了解到使用香草JS进行DOM操作并不是一门科学,实际上许多jQuery方法在本机API中具有直接等效项。
因此,让我们开始吧……
DOM操作:查询DOM
请注意:我不会详细介绍Vanilla DOM API,而只是简单介绍一下。 在用法示例中,您可能会遇到我未明确介绍的方法。 在这种情况下,请参考出色的Mozilla开发人员网络以获取详细信息。
可以使用.querySelector()
方法查询DOM,该方法采用任意CSS选择器作为参数:
const myElement = document.querySelector('#foo > div.bar')
这将返回第一个匹配项(深度优先)。 相反,我们可以检查元素是否与选择器匹配:
myElement.matches('div.bar') === true
如果要获取所有出现的事件,可以使用:
const myElements = document.querySelectorAll('.bar')
如果我们已经有对父元素的引用,则可以查询该元素的子元素,而不是整个document
。 通过这样缩小上下文范围,我们可以简化选择器并提高性能。
const myChildElemet = myElement.querySelector('input[type="submit"]')
// Instead of
// document.querySelector('#foo > div.bar input[type="submit"]')
那么,为什么还要使用那些不太方便的其他方法,例如.getElementsByTagName()
? 好吧,一个重要的区别是.querySelector()
的结果不是实时的 ,因此当我们动态添加与选择器匹配的元素(有关详细信息,请参见第3节 )时,集合不会更新。
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()
立即将所有信息收集到静态列表中,从而降低了性能 。
使用节点列表
现在,关于.querySelectorAll()
有两个常见的陷阱。 第一个是我们不能在结果上调用Node方法并将其传播到其元素(就像您可能在jQuery对象中使用它一样)。 相反,我们必须显式地迭代这些元素。 这是另一个难题:返回值是NodeList,而不是Array。 这意味着通常的Array方法不能直接使用。 有一些对应的NodeList实现,例如.forEach
,但是任何IE仍不支持。 因此,我们必须首先将列表转换为数组,或者从Array原型“借用”这些方法。
// Using Array.from()
Array.from(myElements).forEach(doSomethingWithEachElement)
// Or prior to ES6
Array.prototype.forEach.call(myElements, doSomethingWithEachElement)
// Shorthand:
[].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 // this would be a text node
与任何对象一样,我们可以使用instanceof
运算符检查节点的原型链:
myElement.firstChild.nodeType instanceof Text
修改类和属性
修改元素的类很容易:
myElement.classList.add('foo')
myElement.classList.remove('bar')
myElement.classList.toggle('baz')
免费学习PHP!
全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。
原价$ 11.95 您的完全免费
您可以在Yaphi Berhanu的快速技巧中阅读有关如何修改类的更深入的讨论。 元素属性可以像其他任何对象的属性一样进行访问
// Get an attribute value
const value = myElement.value
// Set an attribute as an element property
myElement.value = 'foo'
// Set multiple properties using Object.assign()
Object.assign(myElement, {
value: 'foo',
id: 'bar'
})
// Remove an attribute
myElement.value = null
请注意,还有方法.getAttibute()
.setAttribute()
和。 removeAttribute()
。 它们直接修改元素的HTML属性 (与DOM属性相对),从而导致浏览器重绘(您可以通过使用浏览器的dev工具检查元素来观察更改)。 这样的浏览器重绘不仅比设置DOM属性更为昂贵,而且这些方法也会产生意想不到的结果 。
根据经验,仅将它们用于没有相应DOM属性的属性(例如colspan
),或者如果您确实要“持久”保留对HTML的更改(例如,在克隆元素或其他元素时保留它们),修改其父级的.innerHTML
(请参阅第3节 )。
添加CSS样式
CSS规则可以像其他属性一样应用; 请注意,尽管这些属性在JavaScript中是驼峰式的:
myElement.style.marginLeft = '2em'
如果我们想要某些值,可以通过.style
属性获得。 但是,这只会给我们明确应用的样式。 要获取计算值,我们可以使用.window.getComputedStyle()
。 它接受元素,并返回CSSStyleDeclaration,其中包含元素本身以及从其父代继承的所有样式:
window.getComputedStyle(myElement).getPropertyValue('margin-left')
修改DOM
我们可以像这样移动元素:
// Append element1 as the last child of element2
element1.appendChild(element2)
// Insert element2 as child of element 1, right before element3
element1.insertBefore(element2, element3)
如果我们不想移动元素,而是插入一个副本,则可以像这样克隆它:
// Create a clone
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和纯文本内容。 它们是可写的属性,这意味着我们可以直接修改元素及其内容:
// Replace the inner HTML
myElement.innerHTML = `
<div>
<h2>New content</h2>
<p>beep boop beep boop</p>
</div>
`
// Remove all child nodes
myElement.innerHTML = null
// Append to the inner HTML
myElement.innerHTML += `
<a href="foo.html">continue reading...</a>
<hr/>
`
如上所述,将标记添加到HTML上通常不是一个好主意,因为我们会丢失之前对受影响的元素进行的属性更改(除非我们将这些更改作为HTML属性保留下来,如第2节所示)和绑定事件侦听器。 设置.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)
听事件
这可能是绑定事件侦听器的最著名方法:
myElement.onclick = function onclick (event) {
console.log(event.type + ' got fired')
}
但这通常应避免。 在这里, .onclick
是元素的属性,意味着可以更改它,但不能使用它添加其他侦听器-通过重新分配新功能,您将覆盖对旧功能的引用。
相反,我们可以使用功能更强大的.addEventListener()
方法添加任意数量的任意类型的事件。 它包含三个参数:事件类型(例如click
),在元素上发生事件时调用的函数(该函数通过事件对象传递)以及可选的config对象(将在下面进行说明)。
myElement.addEventListener('click', function (event) {
console.log(event.type + ' got fired')
})
myElement.addEventListener('click', function (event) {
console.log(event.type + ' got fired again')
})
在侦听器函数中, event.target
指的是在其上触发事件的元素(就像this
,除非我们当然使用箭头函数 )。 因此,您可以轻松地访问其属性,如下所示:
// The `forms` property of the document is an array holding
// references to all forms
const myForm = document.forms[0]
const myInputElements = myForm.querySelectorAll('input')
Array.from(myInputElements).forEach(el => {
el.addEventListener('change', function (event) {
console.log(event.target.value)
})
})
防止默认动作
请注意, event
始终在侦听器函数中可用,但是最好在需要时以任何方式直接传递它(当然,我们可以随便命名它)。 在不详细说明事件接口本身的情况下, .preventDefault()
是一个特别值得注意的方法,它可以防止浏览器的默认行为,例如跟随链接。 另一个常见用例是,如果客户端表单验证失败,则有条件地阻止提交表单。
myForm.addEventListener('submit', function (event) {
const name = this.querySelector('#name')
if (name.value === 'Donald Duck') {
alert('You gotta be kidding!')
event.preventDefault()
}
})
另一个重要的事件方法是.stopPropagation()
,它将防止事件使DOM冒泡。 这意味着,如果我们在某个元素上具有阻止传播的点击监听器(例如说),而在其父元素上具有另一个点击监听器,则在子元素上触发的click事件不会在父元素上触发-否则,两者都会触发。
现在.addEventListener()
将一个可选的config对象作为第三个参数,它可以具有以下任何布尔属性(所有这些属性均默认为false
):
-
capture
:本次活动将在DOM它下面的任何其他元素之前的元素被触发(事件捕获和冒泡是在自己的权利的文章,了解更多详情看看这里 ) -
once
:您可能会猜到,这表明该事件只会触发一次 -
passive
:这意味着event.preventDefault()
将被忽略(通常会在控制台中产生警告)
最常见的选择是.capture
; 实际上,这很普遍,它有一个简写形式:您可以在此处传递一个布尔值,而不是在config对象中指定它:
myElement.addEventListener(type, listener, true)
可以使用.removeEventListener()
删除事件侦听器,该方法采用事件类型和对要删除的回调函数的引用。 例如, once
选项也可以像
myElement.addEventListener('change', function listener (event) {
console.log(event.type + ' got triggered on ' + this)
this.removeEventListener('change', listener)
})
活动委托
另一个有用的模式是事件委托 :说我们有一个表单,想向其所有input
子项添加一个change
事件侦听input
。 一种方法是使用myForm.querySelectorAll('input')
遍历它们,如上所示。 但是,当我们可以将其添加到表单本身并检查event.target
的内容时,这是不必要的。
myForm.addEventListener('change', function (event) {
const target = event.target
if (target.matches('input')) {
console.log(target.value)
}
})
这种模式的另一个优点是,它也自动考虑动态插入的子代,而不必将新的侦听器绑定到每个子代。
动画
通常,执行动画的最干净的方法是应用带有transition
属性CSS类,或使用CSS @keyframes
。 但是,如果您需要更大的灵活性(例如游戏),那么也可以使用JavaScript来完成。
天真的方法是让window.setTimeout()
函数自行调用,直到完成所需的动画为止。 但是,这无法有效地迫使文档快速重排; 而且这种布局颠簸会Swift导致结结,特别是在移动设备上。 Intead,我们可以使用window.requestAnimationFrame()
同步更新,以将所有当前更改安排到下一个浏览器重绘框架。 它以回调为参数,接收当前(高分辨率)时间戳:
const start = window.performance.now()
const duration = 2000
window.requestAnimationFrame(function fadeIn (now)) {
const progress = now - start
myElement.style.opacity = progress / duration
if (progress < duration) {
window.requestAnimationFrame(fadeIn)
}
}
这样,我们可以实现非常流畅的动画。 有关更详细的讨论,请查看Mark Brown的这篇文章。
编写自己的助手方法
的确,与jQuery简洁且可链接的$('.foo').css({color: 'red'})
语法相比,始终必须遍历元素以对它们进行处理可能会比较麻烦。 那么,为什么不简单地为这样的事情编写我们自己的速记方法呢?
const $ = function $ (selector, context = document) {
const elements = Array.from(context.querySelectorAll(selector))
return {
elements,
html (newHtml) {
this.elements.forEach(element => {
element.innerHTML = newHtml
})
return this
},
css (newCss) {
this.elements.forEach(element => {
Object.assign(element.style, newCss)
})
return this
},
on (event, handler, options) {
this.elements.forEach(element => {
element.addEventListener(event, handler, options)
})
return this
}
// etc.
}
}
因此,我们拥有一个超薄的DOM库,其中仅包含我们真正需要的方法,而没有所有向后兼容的权重。 通常,我们通常会在集合的原型中使用这些方法。 这是一个(略为复杂的) 要点 ,其中包含有关如何实现此类助手的一些想法。 或者,我们可以将其简化为
const $ = (selector, context = document) => context.querySelector(selector)
const $$ = (selector, context = document) => context.querySelectorAll(selector)
const html = (nodeList, newHtml) => {
Array.from(nodeList).forEach(element => {
element.innerHTML = newHtml
})
}
// And so on...
演示版
为了使本文更完整,这里有一个CodePen,它演示了上面解释的许多概念,以实现一种简单的灯箱技术。 我鼓励您花一些时间浏览源代码,如果您有任何意见或疑问,请在下面的评论中告诉我。
请参阅CodePen上的SitePoint( @SitePoint ) 笔盒 。
结论
我希望我能证明使用纯JavaScript进行DOM操作不是一门科学,实际上,许多jQuery方法在本机DOM API中具有直接等效项。 这意味着对于某些日常使用案例(例如导航菜单或模式弹出窗口),DOM库的额外开销可能不适当。
而且,虽然本机API的某些部分确实很冗长或不方便(例如必须始终手动遍历节点列表),但我们可以很容易地编写自己的小型辅助函数来抽象出此类重复性任务。
但是现在一切都结束了。 你怎么看? 您是希望避免使用第三方库,还是要自己开发根本不值得花在认知上的开销? 在下面的评论中让我知道。
本文由Vildan Softic和Joan Yin进行了同行评审。 感谢所有SitePoint的同行评审员使SitePoint内容达到最佳状态!
翻译自: https://www.sitepoint.com/dom-manipulation-vanilla-javascript-no-jquery/
vanilla