使用原生 JavaScript 操作 DOM


參考鏈接:https://www.sitepoint.com/dom-manipulation-vanilla-javascript-no-jquery/

微軟官方放棄了對 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);

// 或者使用 `Array.from()`(ES6 中提供的方法)
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 表示深度復制(除了元素本身,元素的孩子也會被復制),為 false 或默認為淺復制(只復制元素本身)。

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>新內容</h2>
    <p>gulu gulu gulu</p>
  </div>
`;

// 刪除 myElement 元素的所有子節點
myElement.innerHTML = null;

// 為 myElement 元素追加內部的 HTML
myElement.innerHTML += `
  <a href="foo.html">繼續閱讀...</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')
}

這種方式只能為某一事件添加唯一的一個事件處理函數。若想添加多個,可以使用 DOM 2 級事件監聽方法 .addEventListener()

5.2 DOM 2 級

myElement.addEventListener('click', function (event) {
  console.log(event.type + ' 事件遭觸發');
})

myElement.addEventListener('click', function (event) {
  console.log(event.type + ' 事件再一次遭觸發');
})

在事件處理函數內部,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(),它阻止事件冒泡至祖先結點。

5.4 .addEventListener() 可選的第三個參數

.addEventListener() 還提供可選的第三個參數,有兩種類型:

  1. 可選得表示配置選項的對象。
  2. 是否在捕獲階段觸發事件的布爾值(默認 false,即在冒泡階段觸發事件)。
// `.addEventListener()` 兩類可選的第三個參數
target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);

5.4.1 配置對象

可選的配置對象有下列 3 個布爾值屬性(默認都為 false):

  1. capture:為 true 時,表示在捕獲階段觸發事件(即到達事件目標之前,會立即觸發事假)。
  2. once:為 true 時,表示事件只能被觸發一次。
  3. passive:為 true 時,會忽略 event.preventDefault() 代碼,不會阻止默認行為的發生(通常會引起控制台發出警告)。

這三個中,最經常使用的是 .capture,這樣,就可以使用可選的表示“是否在捕獲階段觸發事件的布爾值”替代“可選的配置對象”了。

5.4.1 表示是否在捕獲階段觸發事件的布爾值

// 在捕獲階段觸發事件
myElement.addEventListener(type, listener, true);

5.5 .removeEventListener()

刪除事件監聽使用 .removeEventListener()。比如可選的配置對象的 once 屬性可以用 .removeEventListener() 模擬實現。

myElement.addEventListener('change', function listener (event) {
  console.log(event.type + ' 觸發在 ' + this + ' 元素上');
  this.removeEventListener('change', listener);
})

六、事件代理

這是一個很有用的模式。現在有一個表單,當表單元素里的輸入框發生 change 事件時,我們要對此監聽。

myForm.addEventListener('change', function (event) {
  const target = event.target;
  // 監聽表單元素里的輸入框的 `change` 事件時,定義處理邏輯
  if (target.matches('input')) {
    console.log(target.value);
  }
})

這樣的一個好處是,即使表單中的輸入框個數發生了改變,也不會影響監聽事件起作用。

七、動畫

使用 CSS 原生的動畫效果已經很好了(通過 transition 屬性和 @keyframes),但如果需要更加復雜的動畫效果,可以使用 JavaScript。

JavaScript 實現動畫效果,主要有兩種方式:

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

(完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM