微软官方放弃了 IE10-,所以现在可以放心使用原生 JavaScript 操作 DOM 了。

文章针对如下几个点进行介绍:


  1. 查询修改 DOM。
  2. 修改类和特性。
  3. 事件监听
  4. 动画

一、查询 DOM

1.1 ​​.querySelector()​

使用 CSS 选择器获取元素(一个),是网页中符合查询条件的元素快照,不是即时的。


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


1.2 ​​.matches()​

元素是否匹配指定选择器?


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


1.3 ​​.querySelectorAll()​

​.querySelector()​​ 使用 CSS 选择器获取元素(多个),是网页中符合查询条件的元素快照,不是即时的。


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


1.4 在 HTMLElement 元素上使用

​.querySelector()​​​/​​.querySelectorAll()​​​ 不仅可以在 ​​document​​ 上使用,还可以在 HTMLElement 元素上使用。

const myChildElemet = myElement.querySelector('input[type="submit"]');

// 等同于
// document.querySelector('#foo > div.bar input[type="submit"]');

1.5 ​​.getElementsByTagName()​

根据标签来查询元素,是即时的。

// HTML
<div></div>

// JavaScript
const elements1 = document.querySelectorAll('div')
const elements2 = document.getElementsByTagName('div')
const newElement = document.createElement('div')

document.body.appendChild(newElement)
elements1.length // 1
elements2.length // 2

二、操作 NodeList

​.querySelectorAll()​​​ 查询的结果是 ​​NodeList​​​ 类型的,没有法使用数组方法(比如 ​​.forEach()​​ 方法),所以需要:


  1. 把 ​​NodeList​​ 元素装换成数组。
  2. 借用数组的方法。

2.1 把 ​​NodeList​​ 元素装换成数组。


Array.prototype.slice.call(myElements).forEach(doSomethingWithEachElement);

// 或者使用 ES6 方法 `Array.from()`

Array.from(myElements).forEach(doSomethingWithEachElement);


2.2 借用数组的方法


Array.prototype.forEach.call(myElements, doSomethingWithEachElement);

// 或者

[].forEach.call(myElements, doSomethingWithEachElement);


2.3 查询亲属

每个 Element 元素还提供了查询亲属结点的只读属性。


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


可以通过结点的 ​​nodeType​​ 属性值​​,确定结点类型。


myElement.firstChild.nodeType === 3 // 判断是否为文本结点


三、修改类和特性

3.1 ​​.classList​​ API


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


// 获取元素的属性 `value` 的值
const value = myElement.value

// 设置元素的属性 `value` 的值
myElement.value = 'foo'


3.2 ​​Object.assign()​

// 使用 `Object.assign()` 为元素同是设置多个属性
Object.assign(myElement, {
value: 'foo',
id: 'bar'
})

// 删除元素属性
myElement.value = null

​.getAttibute()​​​、​​.setAttribute()​​​ 和 ​​.removeAttribute()​​​ 会直接修改 HTML 特性,会引起浏览器重绘,代价高,不建议使用。如要永久更改 HTML,可以通过使用父元素的 ​​.innerHTML​​ 做到。

3.3 添加 CSS 样式


myElement.style.marginLeft = '2em';


通过 ​​.style​​​ 属性获得的属性值是没有经过计算的。要获取经过计算的值,使用 ​​.window.getComputedStyle()​​。


window.getComputedStyle(myElement).getPropertyValue('margin-left');


四、 修改 DOM

4.1 ​​.appendChild()​​​ 和 ​​.insertBefore()​


// 将 element2 追加为 element1 的最后一个孩子
element1.appendChild(element2);

// 在 element1 的孩子 element3 之前插入 element2
element1.insertBefore(element2, element3);


4.2 ​​.cloneNode()​

要插入一个克隆的元素,可以使用 ​​.cloneNode()​​ 方法。


const myElementClone = myElement.cloneNode();
myParentElement.appendChild(myElementClone);


​.cloneNode()​​​ 还可接收一个布尔值参数,​​true​​ 表示深复制——元素的孩子也会被克隆。

4.3 创建元素


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


myParentElement.removeChild(myElement);

myElement.parentNode.removeChild(myElement);


4.4 ​​.innerHTML​​​ 和 ​​.textContent​

每个元素都有属性 ​​.innerHTML​​​ 和 ​​.textContent​​​(或者类似的 ​​.innerText​​)。


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

// 删除 myElement 元素的所有子节点
myElement.innerHTML = null

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


为元素追加内部的 HTML 并不好,因为会丢失之前的已更改的所有属性和事件监听绑定。

追加元素较好的方式是这样的:


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);


五、事件监听

5.1 DOM 0 级


myElement.onclick = function onclick (event) {
console.log(event.type + ' got fired')
}


这种方式只能为某一事件添加一个事件处理函数。若想添加多个,可以使用 ​​.addEventListener()​​。

5.2 DOM 3 级


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​​)。

5.3 阻止浏览器的默认行为

使用 ​​.preventDefault()​​ 可以阻止浏览器的默认行为(比如点击超链接、提交表单时)。


myForm.addEventListener('submit', function (event) {
const name = this.querySelector('#name');

if (name.value === 'Donald Duck') {
alert('You gotta be kidding!');
event.preventDefault();
}
})


另外一个重要的方法是 ​​.stopPropagation()​​——阻止事件冒泡至祖先结点。

绑定监听事件时,还可以指定第三个参数:可选的配置对象/是否在捕获阶段触发事件的布尔值(默认 ​​false​​,即在冒泡阶段触发事件)。

5.4 ​​.addEventListener()​​ 可选的第三个参数


target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);


可选的配置对象有下列 3 个布尔值属性(默认都为 ​​false​​):


  1. ​capture​​​:为 ​​true​​ 时,表示在捕获阶段触发事件(即到达事件目标之前,会立即触发事假)。
  2. ​once​​​:为 ​​true​​ 时,表示事件只能被触发一次。
  3. ​passive​​​:为 ​​true​​​ 时,会忽略 ​​event.preventDefault()​​ 代码,不会阻止默认行为的发生(通常会引起控制台发出警告)。

这三个中,最经常使用的是 ​​.capture​​,这样,就可以使用可选的表示“是否在捕获阶段触发事件的布尔值”替代“可选的配置对象”了。


// 在捕获阶段触发事件
myElement.addEventListener(type, listener, true);


5.5 ​​.removeEventListener()​

删除事件监听使用 ​​.removeEventListener()​​​。比如可选的配置对象的 ​​once​​ 属性可以这样实现:


myElement.addEventListener('change', function listener (event) {
console.log(event.type + ' got triggered on ' + this);
this.removeEventListener('change', listener);
})


六、事件代理

这是一个很有用的模式。现在有一个表单,当表单元素里的输入框发生 ​​change​​ 事件时,我们要对此监听。


myForm.addEventListener('change', function (event) {
const target = event.target;
if (target.matches('input')) {
console.log(target.value);
}
})


这样的一个好处是——即是元素中的输入框个数发生了改变,也不会影响监听事件起作用。

七、动画

使用 CSS 原生的动画效果已经很好了(通过 ​​transition​​​ 属性和 ​​@keyframes​​),但如果需要更加复杂的动画效果,可以使用 JavaScript。

JavaScript 实现动画效果,主要有两种方式:


  1. ​window.setTimeout()​​​:在动画完成后,停止对 ​​window.setTimeout()​​ 的调用,但可能会出现动画不连续。
  2. ​window.requestAnimationFrame()​​:


const start = window.performance.now();
const duration = 4000;

window.requestAnimationFrame(function fadeIn (now) {
const progress = now - start;
myElement.style.opacity = progress / duration;

if (progress < duration) {
window.requestAnimationFrame(fadeIn);
}
})


八、写辅助方法

第一种方式:


const $ = function $ (selector, context = document) {

const elements = (selector, context = document) => context.querySelectorAll(selector);
const element = elements[0];

return {
element,
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;
}
};

};


第二种方式:


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;
})
}


(完)