Mike

30 June, 2025

深入了解 DOM 操作原理

DOM (文件物件模型)

文件物件模型(Document Object Model,Web 的程式界面。它提供了一個樹狀結構化表示法,並讓程式可以存取並修改網頁的架構、風格(style)和內容(value)等等。

DOM 介面

Attr DOMTimeStamp NodeIterator
CharacterData DOMSettableTokenList NodeList
ChildNode DOMStringList ParentNode
Comment DOMTokenList ProcessingInstruction
CustomEvent Element Range
Document Event Text
DocumentFragment EventTarget TreeWalker
DocumentType HTMLCollection URL
DOMError MutationObserver Window
DOMException MutationRecord Worker
DOMImplementation Node XMLDocument
DOMString NodeFilter NodeIterator

網頁加載時,瀏覽器會根據 DOM 模型,將 HTML 解析成一系列的節點,再由這些節點組成一個樹狀結構(DOM Tree)。

DOM Tree

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <link rel="stylesheet" href="">
</head>
<body>
  <p>Hello<span>web performance</span>students</p>
  <div><a>url link</a></div>
</body>
</html>

上面是一段非常簡單的 html 架構,我們來看看轉換過來的 DOM tree

CSSOM

CSSOM 是一組允許 JavaScript 操作 CSS 的 API。它非常類似於 DOM,但是用於 CSS 而不是 HTML。它允許用戶動態讀取和修改 CSS 樣式。

Render Tree

為了將 DOM 與 CSSOM 組合成 Render Tree,瀏覽器大致需要執行下列步驟:

  1. 從 DOM tree 的根節點開始,瀏覽每個可見的節點。
    • 某些節點完全無法察覺 (例如 script 標記、meta 標記等)。由於轉譯的輸出內容中不會反映這些節點,因此會被省略。
    • 某些節點透過 CSS 隱藏起來,在 Render Tree 也會被省略。以上述示例的 span 節點說明,由於該節點透過顯式規則設定了 display:none 屬性,因此不會出現在 Render Tree 中。
  2. 為每個可見的節點找到相符的 CSSOM 規則,並套用這些規則。
  3. 發送可見的節點,包括內容和計算的的樣式。

visibility: hidden 和 display: none 的差異:

  • visibility: hidden 會隱藏元素,這個元素會佔據版面配置的相應空間。
  • display: none 會直接從 Render tree 中徹底移除元素,該元素不僅會消失,而且也不再屬於版面配置的一部份。

最終輸出的 Render Tree 不僅包含螢幕上顯示的所有可見內容,同時也包含相應的樣式資訊。我們就快要大功告成了!有了 Render Tree,我們就能進入「版面配置」階段。

<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Critical Path: Hello world!</title>
</head>
<body>
  <div style="width: 50%">
    <div style="width: 50%">Hello world!</div>
  </div>
</body>
</html>
 

上網頁的內文包含兩個嵌套的 div:第一個 div 寬度設 50%,第二個 div 寬度設定 50%,即為檢視區寬度的 25%!

版面配置過程的輸出結果稱為 box model (盒子模型),這個模型可精確說明每個元素在檢視區中的準確位置和大小,所有相對度量單位都會轉換為螢幕上的絕對像素位置。

知道哪些節點可見、計算的樣式和幾何形狀之後,我們終於可以將這些資訊傳遞到最後一個階段,將 Render Tree 中的每個節點轉換為螢幕上的實際像素。這個步驟通常稱為「繪製」或者「點陣化」。

Render Tree 的建構、版面配置和繪製所需的時間取決於文件的大小、套用的樣式,當然還有執行文件的裝置,文件越大,瀏覽器需要完成的工作就越多,樣式越複雜,繪製所需的時間就越長。

 

節點物件類型

以下是 html 最常見的節點類型:

nodeType 節點分類
DOCUMENT_NODE 例 window.document
ELEMENT_NODE 例 <body>、<a>、<p>、<script>、<style>、<html>、<h1>、<span>
TEXT_NODE 例如 html 文件中的文字字元,包括回車 return 與空格。
DOCUMENT_FRAGMENT_NODE 例 document.createDocumentFragment()
DOCUMENT_TYPE_NODE 例 <!DOCTYPE html>

所有的節點物件都會從主 Node 物件繼承屬性與方法,這些屬性和方法都是可以操作的,除了節點提供的屬性跟方法外,也有許多子節點介面提供的屬性與方法。

 

常見的節點處理屬性與方法

節點屬性

  • childNodes
  • firstChild
  • lastChild
  • nextSibling
  • nodeName
  • nodeType
  • nodeValue
  • parentNode
  • previousSibling

節點方法

  • appendChild()
  • cloneNode()
  • compareDocumentPosition()
  • contains()
  • hasChildNodes()
  • insertBefore()
  • isEqualNode()
  • removeChild()
  • replaceChild()

文件方法

  • Document.createElement()
  • Document.createTextNode()

HTML Element 屬性

  • innerHTML
  • outerHTML
  • textContent
  • innerText
  • outerText
  • firstElementChild
  • lastElementChild
  • nextElementChild
  • previousElementChild
  • children

HTML元素方法

  • insertAdjacentHTML()

範例下載:https://github.com/MikeCheng1208/JS_course_dom_example

HTML Element 常見的屬性方法

方法/屬性 說明
createElement() 建立元素
tagName 取得元素標籤名稱
children 獲取子元素內容
getAttribute() 取得元素的屬性值
setAttribute() 設定元素的屬性值
hasAttribute() 驗證元素是否存在
removeAttribute() 移除元素的屬性值
classList() class 的操作類別屬性跟移除
dataset 取得與設定 data-* 屬性
attribute 取得元素屬性與値的清單

實作範例

01 - dynamic

就讓我們試著動態的建立元素節點以及載入 script

const script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.setAttribute('src', './jquery.min.js');
document.getElementsByTagName('head')[0].appendChild(script);
script.onload = e => {
    console.log($);
}

appendChild:將指定的節點,放到父物件的尾端

02-createNode

const el = document.createElement('h1')
const textNode = document.createTextNode('createNode');
el.appendChild(textNode);

document.getElementsByTagName('body')[0].appendChild(el);

03-removeNode

html

<div id="app">
  <h1 id="tit1">DOM 1</h1>
  <h2 id="tit2">DOM 2</h2>
  <h3 id="tit3">DOM 3</h3>
  <h4 id="tit4">DOM 4</h4>
</div>

JavaScript

const app = document.getElementById('app');

// 移除元素節點
const tit2 = document.getElementById('tit2');
app.removeChild(tit2);

// 移除文字節點
const tit3txt = document.getElementById('tit3').firstChild;
document.getElementById('tit3').removeChild(tit3txt);

// 塞入文字節點
const txtNode = document.createTextNode("new Tit3");
document.getElementById('tit3').appendChild(txtNode);

04-document-Info

const { title, URL, lastModified } = document;
console.log({ title, URL, lastModified });

輸出結果:

{
    URL: "http://127.0.0.1:5500/04-document-Info/index.html"
    lastModified: "10/18/2019 20:16:02"
    title: "document Info"
}
屬性 說明
doctype <!DOCTYPE html>
documentElement <html lang="en">
activeElement 取得文件中聚焦,使用中的元素
body <body>
head <head>
title <title>
lastModified 最後修改文件的時間
URL 網址

稍微列出幾個常見的 document 屬性,當然不只這幾個,有興趣可以再去官網查,只是幾乎沒有用到的機會…

05 - insertBefore

const ul = document.getElementById('ul');
const refNode = document.querySelectorAll('li')[2];
const newNode = document.createElement('li');
const textNode = document.createTextNode("Webpack 前端自動化開發");
newNode.appendChild(textNode);
ul.insertBefore(newNode, refNode);

將新節點(newNode)插入至指定的( refNode )節點的前面

06 - replaceChild

const ul = document.getElementById('ul');
const oldNode = document.querySelectorAll('li')[2];
const newNode = document.createElement('li');
const textNode = document.createTextNode("Vue3.0 框架實戰");
newNode.appendChild(textNode);
ul.replaceChild(newNode, oldNode);

將原本的舊的節點替換成指定的節點

07- createElement & appendChild

const data = [
    { name: "mike", age: 22 },
    { name: "jacky", age: 12 },
    { name: "andy", age: 33 },
    { name: "kuro", age: 50 }
]
const body = document.querySelector('body')
const arr = [];
let isRe = false;

for (let item of data) {
    const h1 = document.createElement("h1");
    const textNode = document.createTextNode(`${item.name} ${item.age}`);
    h1.age = item.age;
    h1.append(textNode);
    arr.push(h1);
}
const SortFn = () =>{
    isRe = !isRe;
    if(isRe){
        arr.sort((a, b)=> a.age - b.age);
    }else{
        arr.sort((a, b)=> b.age - a.age);
    }
}
const appendDOM = () =>{
    SortFn();
    for (let item of arr) {
        body.appendChild(item);
    }
}
document.querySelector('#RenderBtn').addEventListener("click", ()=>{
    appendDOM();
})

重要概念:The Node.appendChild() method adds a node to the end of the list of children of a specified parent node. If the given child is a reference to an existing node in the document, appendChild() moves it from its current position to the new position (there is no requirement to remove the node from its parent node before appending it to some other node).

appendChild 使用時,append 上去的是一個已存在的節點時,它會做的是搬移,而非複製

參考:https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild

08- setAttribute

const h1 = document.createElement('h1');
const txt = document.createTextNode('Mike');
h1.appendChild(txt);
document.querySelector('body').appendChild(h1);

h1.setAttribute('id', 'titleName');
h1.setAttribute('data-id', 0);
h1.style.cssText = 'color: red;';

09- package

dom.js

function creEle(tag){
    this.el = document.createElement(tag);
    this.setText = function(context){
        const textNode = document.createTextNode(context);
        this.el.append(textNode);
    }
    this.setAttribute = function(attribute){
        for (let attr in attribute) {
            if(attr === 'style'){
                this.el.style.cssText += attribute[attr];
            }else{
                this.el.setAttribute(attr, attribute[attr]);
            }
        }
    }
}

Index.html

const h1 = new creEle('h1');
h1.setText('JavaScript');
document.querySelector('body').appendChild(h1.el);

10-appendChild vs innerHTML

innerHTML

const List = document.querySelector('.List');
const sortBtn = document.querySelector('.sort');
let html = "";
const tableData = JSON.parse(JSON.stringify(TABLE_DATA));

const renderDOM = () => {
  html = "";
  tableData.forEach(item => {
    html += `
      <ul>
        <li class="id">${item.id}</li>
        <li class="image"><img src="${item.thumbnailUrl}" alt=""></li>
        <li class="name">??</li>
        <li class="price">${item.price}</li>
      </ul>
    `;
  });
  List.innerHTML = html;
};

renderDOM();

sortBtn.addEventListener("click", () => {
  tableData.sort((a, b) => a.price - b.price);
  renderDOM();
});

 

appendChild

const List = document.querySelector('.List');
const sortBtn = document.querySelector('.sort');
const tableData = JSON.parse(JSON.stringify(TABLE_DATA));
const dataArr = [];
let isReverse = false;
const createDOM = () =>{
    tableData.forEach(item=>{
        const ul = new creEle('ul');
        const idli = new creEle('li');
        const imageli = new creEle('li');
        const img = new creEle('img');
        const nameli = new creEle('li');
        const priceli = new creEle('li');
        idli.setText(item.id);
        img.setAttribute({'src': item.thumbnailUrl});
        imageli.el.appendChild(img.el);
        nameli.setText(item.name);
        priceli.setText(item.price);
        idli.setAttribute({'class': 'id'});
        imageli.setAttribute({'class': 'image'});
        nameli.setAttribute({'class': 'name'});
        priceli.setAttribute({'class': 'price'});
        ul.el.appendChild(idli.el);
        ul.el.appendChild(imageli.el);
        ul.el.appendChild(nameli.el);
        ul.el.appendChild(priceli.el);
        dataArr.push(ul.el);
    });
}
const renderDOM = () =>{
    dataArr.forEach(item=>document.querySelector('.List').appendChild(item))
}
createDOM();
renderDOM();
sortBtn.addEventListener("click", ()=>{
    isReverse = !isReverse;
    if(isReverse){
        dataArr.sort((a, b)=> (+a.childNodes[3].textContent) - (+b.childNodes[3].textContent));
    }else{
        dataArr.sort((a, b)=> (+b.childNodes[3].textContent) - (+a.childNodes[3].textContent));
    }
    renderDOM();
})

我們來看一下 appendChild 跟 innerHTML 效能差距…

appendChild 範圍 : 972 ms – 1.10 s
innerHTML 範圍 : 1.22 s – 1.43 s

 

瀏覽器渲染過程與優化

現在大多數的行動裝置會以每秒 60 次更新率重整螢幕,如果正在執行網頁的動畫或是捲動網頁,瀏覽器就會需要符合裝置的重整頻率,並針對每一次螢幕重整,組成一個新圖形或畫面。

渲染的五大流程

  1. JavaScript:通常用來處理視覺的工作,無論是動畫函式庫、動態排序資料列表,或新增 DOM 至網頁。
  2. Style (樣式):根據符合的 CSS 選取器,瀏覽器會解析 CSS 元素規則的過程,例如 .headline 或 .nav > .nav__item。一旦解析完成,它們就會被套用,並計算每個元素的樣式。
  3. Layout (佈局):一旦瀏覽器知道了套用哪些的元素規則,它就可以開始計算要佔用多少空間,以及它在螢幕上的位置。元素之間會互相影響,例如 <body> 的寬度通常會影響其子物件的寬度,以及在樹狀結構往上或往下延伸。
  4. Paint (繪製):繪製是以像素為單位填入。牽涉到描繪出文字、顏色、影像、邊框和陰影,基本上是元素的每個視覺化部分。
  5. Composite (合成):因為網頁的描繪可分成多個層級,因此它們需要按正確的順序渲染至螢幕,讓網頁能正確轉譯。

JavaScript 或 CSS 針對網頁畫面去做調整,大略來說分成 3 種渲染方式!

1. JS / CSS > Style > Layout > Paint > Composite

當你操作跟 Layout 有相關的屬性,像是 width、height 或是其他 top、left 屬性時,頁面都會重新繪製佈局一次,任何受影響的區域都會重新繪製。

2. JS / CSS > Style > Paint > Composite如果是操作 Paint 相關屬性,例如 background-image、text、color 或 shadows,一些跟 Layout 無關的屬性時,瀏覽器會略過 Layout 由 paint 開始更新頁面。

3. JS / CSS > Style > Composite

如果避開操作 Layout,也不畫 Paint,那麼瀏覽器會直接跳去 Composite,這是效能最好的處理方式,也是在整個效能調校中最重要的點,尤其是在大量動畫處理或是畫面捲動時更看得出來差異!

Css Triggers

可以透過 css triggers 來查詢 css 在被更改的時候會被觸發的渲染狀態!

網址:https://csstriggers.com/

效能好的動畫應該避免 Layout 、 Paint 只需要 Composite

為了達成此目標,目前只能靠 transform 和 opacity 這兩個方法

compositor layer

可以在不影響其他元素的情況下進行重新繪製,我們需要將動畫處理的元件提升至 compositor layer,來確保動畫執行的時候週遭的元件不會進行重繪。

.element {
    will-change: transform;
}

針對較舊的瀏覽器,不支援 will-change 的話

.element {
    transform: translateZ(0);
}

請不要貪圖方便,這不是一個好做法

* {
    will-change: transform;
    transform: translateZ(0);
}

這樣會導致每一個在網頁上面的元件在瀏覽器中消耗無謂的記憶體

開啟階層面板

11-Reflow and Repaint

  • left

css

.box{
    position: fixed;
    width: 100px;
    height: 100px;
    background-color: red;
    top: 40px;
    left: 5px;
    transition: left 1s;
}
.box.aminate{
    left: 300px;
}

JavaScript

const btn = document.querySelector('#btn');
const box = document.querySelector('.box');
btn.addEventListener('click', ()=>{
    box.classList.add('aminate');
})

 

  • transform

css

.box{
    position: fixed;
    width: 100px;
    height: 100px;
    background-color: red;
    top: 40px;
    left: 5px;
    transition: transform 1s;
    will-change: transform;
    transform: translateZ(0);
}
.box.aminate{
    transform: translateX(300px);
}

JavaScript

const btn = document.querySelector('#btn');
const box = document.querySelector('.box');
btn.addEventListener('click', ()=>{
    box.classList.add('aminate');
})

效能比較

Chrome DevTools Performance

使用 Chrome DevTools 的 performance 面板可以記錄和分析您的應用在運行時的所有活動。

模擬行動裝置的 CPU

面板介紹

  1. 開始記錄,停止記錄和配置記錄面板。
    • 儘可能保持記錄簡短:簡短的記錄通常會讓分析更容易。
    • 避免不必要的操作:避免與您想要記錄和分析的活動無關聯的操作(滑鼠點擊、網路加載等等)。
    • 停用擴充應用程式:Chrome 擴展程序會給應用的 performance 記錄增加不相關的噪聲。
  2. 頁面性能的 Overview:
    • FPS:每秒幀數。綠色豎線越高,FPS 越高。FPS 圖表上的紅色塊表示長時間幀,很可能會出現卡頓。
    • CPU:此面積圖指示消耗 CPU 資源的事件類型。
    • NET:每條彩色橫槓表示一種資源。橫槓越長,搜尋資源所需的時間越長。每個橫槓的淺色部分表示等待時間(從請求資源到第一個字節下載完成的時間)。
  3. CPU 堆疊追蹤的圖像化。

    事件 說明
    Parse HTML Chrome 執行 HTML 解析算法
    Event JS 事件,例如 click
    Layout 頁面布局已被執行
    Recalculate style Chrome 重新計算元素樣式
    Paint 合成的圖層被繪製到顯示畫面的一個區域
    Composite Chrome 的渲染引擎合成了圖像層
  4. 選擇區間範圍後,會顯示該範圍操作的細節圓餅圖。
    • Loading:載入時間
    • Scripting:腳本運算時間
    • Rendering:渲染時間
    • Painting:繪圖時間
    • System:其他時間
    • Idle:流覽器閒置時間
    • Total:總時間

 

12-Throttle

為了避免我們調用 onscroll 或是 onresize 事件高平頻率觸發,消耗大量的網頁的效能,所以我們需要設定一個節流閥,來避免高頻率操作。

let reTimes = null
function ThrottleFn() {
    if(reTimes){
        clearTimeout(reTimes)
    }
    reTimes = setTimeout(()=>{
        console.log('scroll');
    },400);
}
ThrottleFn();
window.addEventListener('scroll', ThrottleFn);

我們可以使用 setTimeout 設定一個時間間隔,這樣就可以避免每個毫秒都再觸發,是非常好用的優化方式。

我們可以把它封裝起來,已利於重複使用

function ThrottleFn(fn=()=>{}, timer=400) {
    let reTimes = null
    return ()=>{
        if(reTimes){
            clearTimeout(reTimes)
        }
        reTimes = setTimeout(()=>{
            fn();
        }, timer);
    }
}

可以透過閉包的特性,來存取跟移除上一個計時器,我們來看一下如何調用。

function ThrottleFn(fn=()=>{}, timer=400) {
    let reTimes = null
    return ()=>{
        if(reTimes){
            clearTimeout(reTimes)
        }
        reTimes = setTimeout(()=>{
            fn();
        }, timer);
    }
}

function scrollLog(){
    console.log('scroll');
}

const scrollFn = ThrottleFn(scrollLog, 500);
scrollFn();

window.addEventListener('scroll', scrollFn);

13 - LazyLoad

Throttle.js

function ThrottleFn(fn=()=>{}, timer=400) {
    let reTimes = null
    return ()=>{
        if(reTimes){
            clearTimeout(reTimes)
        }
        reTimes = setTimeout(()=>{
            fn();
        }, timer);
    }
}

Html

<div>
  <img data-src="https://source.unsplash.com/random/800x800?1" class="lazyload" />
</div>
<div>
  <img data-src="https://source.unsplash.com/random/800x800?2" class="lazyload" />
</div>
<div>
  <img data-src="https://source.unsplash.com/random/800x800?3" class="lazyload" />
</div>
<div>
  <img data-src="https://source.unsplash.com/random/800x800?4" class="lazyload" />
</div>
<div>
  <img data-src="https://source.unsplash.com/random/800x800?5" class="lazyload" />
</div>
Css
.lazyload, .lazyloading {
    opacity: 0;
}
.lazyloaded {
    transition: opacity 1s;
    opacity: 1;
}

javaScript

const lazyImages = document.querySelectorAll('.lazyload');
function lazyloadFn() {
    lazyImages.forEach(image => {
        if (image.offsetTop < window.innerHeight + window.pageYOffset + 300) {
            if(image.src === ""){
                image.src = image.dataset.src;
                image.onload = () => image.classList.add('lazyloaded')
            }
        }
    })
}
const lazy = ThrottleFn(lazyloadFn);
lazy();
window.addEventListener('scroll', lazy);

網頁占最大資源的就是圖片,當網站圖片較多時,會造成網站開啟速度變慢。Lazyload 延遲加載是一種使用者剛好滑到圖片位置時,才顯示圖片的方式,達到節省資源、加速網頁速度的效果。

14 - getTarget

html

<ul class="ul-List">
  <li>JavaScript & TypeScript 前端工程師入門班</li>
  <li>Vue3 高效入門與實踐</li>
  <li>Nuxt3 高效入門全攻略</li>
  <li>超越入門!Webpack 前端自動化開發</li>
  <li>Vue 單元測試 vue-test-utils|入門</li>
</ul>

getElementsByClassName

// 取得 ul 元件
document.getElementsByClassName('ul-List')[0];
// 取得 ul 裡面的 li 子元件
document.getElementsByClassName('ul-List')[0].children[2];

querySelector

// 取得 ul 元件
document.querySelector('.ul-List');
// 取得 ul 裡面的 li 子元件
document.querySelector('.ul-List > li:nth-child(3)');

querySelector 可以用 css3 選擇器來選取 DOM 元素。

15 - getElementsByClassName vs querySelectorAll

getElementsByClassName

const item = document.getElementsByClassName('item');
console.log(item); // ----> 4

// -------------------------------------------------------------
const li = document.createElement('li');
const liTxt = document.createTextNode('TweenMax 動態特效速成實戰');
li.setAttribute('class', 'item');
li.appendChild(liTxt);
document.getElementsByClassName('ul-Box')[0].appendChild(li);
// ---------------------------------------------------------------

console.log(item); // ----> 5

querySelectorAll

const item = document.querySelectorAll('.item');
console.log(item); //----> 4

// ---------------------------------------------------------------
const li = document.createElement('li');
const liTxt = document.createTextNode('TweenMax 動態特效速成實戰');
li.setAttribute('class', 'item');
li.appendChild(liTxt);
document.querySelector('.ul-Box').appendChild(li);
// ----------------------------------------------------------------

console.log(item); //----> 4

JavaScript 插入與執行

預設情況下,javaScript 會被同步解析,當瀏覽器解析 DOM 遇到<script>標籤的時候,遇設會,執行 JavaScript,因為這是停止的行為,且不行平行解析其它 DOM 與執行 JavaScript,它被視為同步的動作。

16-1 - script-tag

defer 來延遲載入 javaScript 與執行

16-2 - defer

async 來非同步載入 javaScript 執行

16-3 — async(IE9 不支援 async)

  • IE9 不支援 async
  • async 檔案會平行載入,完全下載後會依照順序解析
  • async 與 defer 同時使用 async 會蓋過 defer

17-preventDefault

HTML

<form action="https://www.google.com/" method="GET">
  <input type="text" name="name" id="name">
  <button id="btn">送出</button>
</form>

<a id="alink" href="#">link</a>

JavaScript

document.getElementById('btn').addEventListener('click', e=>{
    //阻止瀏覽器預設行為
    e.preventDefault();
});

document.getElementById('alink').addEventListener('click', e=>{
    //阻止瀏覽器預設行為
    e.preventDefault();
});

18-stopPropagation

HTML

<ul>
  <li>JavaScript & TypeScript 前端工程師入門班</li>
  <li>Vue3 高效入門與實踐</li>
  <li>Nuxt3 高效入門全攻略</li>
  <li>超越入門!Webpack 前端自動化開發</li>
  <li>Vue 單元測試 vue-test-utils|入門</li>
</ul>

javaScript

const item = document.querySelectorAll('.item');
const link = document.querySelectorAll('.item > a');

for(let i = 0; i < item.length; i++){
    item[i].addEventListener('click', getItemEl);
    link[i].addEventListener('click', getlinkEl);
}

function getItemEl(e){
    console.log("1");
}

function getlinkEl(e){
    e.stopPropagation();
    console.log("2");
}

停止事件在傳播過程的捕獲、目標處理或冒泡階段進一步傳播。

Event dispatch and DOM event flow

參考:https://www.w3.org/TR/DOM-Level-3-Events/#event-flow

19-誤差的毫秒

Html

<h1>0</h1>

javascript

let startTime = new Date().getTime();
let t = 0;
const h1 = document.querySelector('h1') ;

const timer = () =>{
    let endTime = new Date().getTime();
    t++;
    h1.innerText = t;
    console.log((endTime - startTime) + ' ms');
}

setInterval(timer, 1000);

瀏覽器會因為程式執行時間造成 setInterval 或 setTimeout 的時間有誤差

輸出結果範例:

 

19-2 修正誤差的毫秒

let startTime = new Date().getTime()
let t = 0;
const h1 = document.querySelector('h1') ;

setInterval(()=>{
    let endTime = new Date().getTime() - startTime;
    t = Math.floor(endTime / 100) / 10 | 0;
    h1.innerText = t;
    console.log(t);
}, 100);

公式:((現在時間 – 開始時間) / 執行毫秒) / 10 | 0

  • 10:進位
  • | 0:去小數點

這種方法並不能 100%完全解決時間誤差的問題,但是可以對些許的時間作為調整,需要精準的對時間作調整可以考慮用這種方法來調整。

20-MutationObserver

Html

<div id="app"> 
     <!-- MutationObserver 可以監控DOM變化 --> 
</div>

JavaScript

const app = document.querySelector("#app");
const mo = new MutationObserver(mutations=> {
    mutations.forEach(mutation=> {
        console.log(mutation);
    });
});
mo.observe(app, {
    attributes: true,
    characterData: true,
    childList: true,
    subtree: true,
    attributeOldValue: true,
    characterDataOldValue: true
});

const a_link = document.createElement('a');
const txt_Node = document.createTextNode('BTN');
a_link.setAttribute('href', 'javascript:;')
a_link.appendChild(txt_Node);

app.appendChild(a_link);

MutationObserver 可以監控 DOM 變化

MutationObserver 三個方法
方法 說明
observe 開始監聽
disconnect 停止監聽
takeRecords 清除變動記錄

.observe(target, options);

  • target: 要監控的 DOM 元素
  • options: 監控的類型
Options
選項 說明
childList 子節點的變動(指新增,刪除或者更改)
attributes 屬性的變動
characterData 節點內容或節點文本的變動
subtree 表示是否將該觀察器應用於該節點的所有後代節點
attributeOldValue 表示觀察 attributes 變動時,是否需要記錄變動前的屬性值
characterDataOldValue 表示觀察 characterData 變動時,是否需要記錄變動前的值
attributeFilter 表示需要觀察的特定屬性(例:['class','src'])
 
DOM 變動後回傳相關資訊
屬性 說明
type 觀察的變動類型(attribute、characterData 或者 childList)
target 發生變動的 DOM 節點
addedNodes 新增的 DOM 節點
removedNodes 刪除的 DOM 節點
previousSibling 前一個同級節點,如果沒有則返回 null
nextSibling 下一個同級節點,如果沒有則返回 null
attributeName 發生變動的屬性。如果設置了 attributeFilter,則只返回預先指定的屬性。
oldValue 變動前的值。這個屬性只對 attribute 和 characterData 變動有效,如果發生 childList 變動,則返回 null
 
注意事項
  1. 當需要結束監控時可呼叫 .disconnect()
  2. 非必要不要綁定整個 document.documentElement

你或許再開發一個所見即所得的服務,嘗試實踐【上一步】或【拖拉 DOM】等功能,我們可以利用 MutationObserver API,知道使用者進行了哪些修改,因此可以輕鬆地撤消這些動作。


 

總結

DOM 操作是前端開發的基礎,掌握正確的操作方法和優化技巧對於建立高效能的 Web 非常重要。
最好的優化往往來自於理解瀏覽器的工作原理,然後據此調整你的程式碼。

不斷實踐這些技巧,並使用 Chrome DevTools 來驗證你的優化效果。隨著經驗的累積,你將能夠寫出更高效、更流暢的前端 Web。

在我的課程中就會有許多這樣的小知識點的教學,想一起學習更多 JavaScript 
現在就加入 👉 https://thecodingpro.com/courses/javascript