文件物件模型(Document Object Model,Web 的程式界面。它提供了一個樹狀結構化表示法,並讓程式可以存取並修改網頁的架構、風格(style)和內容(value)等等。
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)。
<!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 是一組允許 JavaScript 操作 CSS 的 API。它非常類似於 DOM,但是用於 CSS 而不是 HTML。它允許用戶動態讀取和修改 CSS 樣式。
為了將 DOM 與 CSSOM 組合成 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%!
知道哪些節點可見、計算的樣式和幾何形狀之後,我們終於可以將這些資訊傳遞到最後一個階段,將 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 物件繼承屬性與方法,這些屬性和方法都是可以操作的,除了節點提供的屬性跟方法外,也有許多子節點介面提供的屬性與方法。
方法/屬性 | 說明 |
---|---|
createElement() | 建立元素 |
tagName | 取得元素標籤名稱 |
children | 獲取子元素內容 |
getAttribute() | 取得元素的屬性值 |
setAttribute() | 設定元素的屬性值 |
hasAttribute() | 驗證元素是否存在 |
removeAttribute() | 移除元素的屬性值 |
classList() | class 的操作類別屬性跟移除 |
dataset | 取得與設定 data-* 屬性 |
attribute | 取得元素屬性與値的清單 |
就讓我們試著動態的建立元素節點以及載入 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:將指定的節點,放到父物件的尾端
const el = document.createElement('h1')
const textNode = document.createTextNode('createNode');
el.appendChild(textNode);
document.getElementsByTagName('body')[0].appendChild(el);
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);
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 屬性,當然不只這幾個,有興趣可以再去官網查,只是幾乎沒有用到的機會…
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 )節點的前面
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);
將原本的舊的節點替換成指定的節點
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
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;';
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);
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 效能差距…
現在大多數的行動裝置會以每秒 60 次更新率重整螢幕,如果正在執行網頁的動畫或是捲動網頁,瀏覽器就會需要符合裝置的重整頻率,並針對每一次螢幕重整,組成一個新圖形或畫面。
可以透過 css triggers 來查詢 css 在被更改的時候會被觸發的渲染狀態!
可以在不影響其他元素的情況下進行重新繪製,我們需要將動畫處理的元件提升至 compositor layer,來確保動畫執行的時候週遭的元件不會進行重繪。
.element {
will-change: transform;
}
針對較舊的瀏覽器,不支援 will-change 的話
.element {
transform: translateZ(0);
}
請不要貪圖方便,這不是一個好做法
* {
will-change: transform;
transform: translateZ(0);
}
這樣會導致每一個在網頁上面的元件在瀏覽器中消耗無謂的記憶體
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');
})
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 面板可以記錄和分析您的應用在運行時的所有活動。
事件 | 說明 |
---|---|
Parse HTML | Chrome 執行 HTML 解析算法 |
Event | JS 事件,例如 click |
Layout | 頁面布局已被執行 |
Recalculate style | Chrome 重新計算元素樣式 |
Paint | 合成的圖層被繪製到顯示畫面的一個區域 |
Composite | Chrome 的渲染引擎合成了圖像層 |
為了避免我們調用 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);
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>
.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 延遲加載是一種使用者剛好滑到圖片位置時,才顯示圖片的方式,達到節省資源、加速網頁速度的效果。
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>
// 取得 ul 元件
document.getElementsByClassName('ul-List')[0];
// 取得 ul 裡面的 li 子元件
document.getElementsByClassName('ul-List')[0].children[2];
// 取得 ul 元件
document.querySelector('.ul-List');
// 取得 ul 裡面的 li 子元件
document.querySelector('.ul-List > li:nth-child(3)');
querySelector 可以用 css3 選擇器來選取 DOM 元素。
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
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 會被同步解析,當瀏覽器解析 DOM 遇到<script>標籤的時候,遇設會,執行 JavaScript,因為這是停止的行為,且不行平行解析其它 DOM 與執行 JavaScript,它被視為同步的動作。
HTML
JavaScript
document.getElementById('btn').addEventListener('click', e=>{
//阻止瀏覽器預設行為
e.preventDefault();
});
document.getElementById('alink').addEventListener('click', e=>{
//阻止瀏覽器預設行為
e.preventDefault();
});
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");
}
停止事件在傳播過程的捕獲、目標處理或冒泡階段進一步傳播。
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 的時間有誤差
輸出結果範例:
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
這種方法並不能 100%完全解決時間誤差的問題,但是可以對些許的時間作為調整,需要精準的對時間作調整可以考慮用這種方法來調整。
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);
方法 | 說明 |
---|---|
observe | 開始監聽 |
disconnect | 停止監聽 |
takeRecords | 清除變動記錄 |
.observe(target, options);
選項 | 說明 |
---|---|
childList | 子節點的變動(指新增,刪除或者更改) |
attributes | 屬性的變動 |
characterData | 節點內容或節點文本的變動 |
subtree | 表示是否將該觀察器應用於該節點的所有後代節點 |
attributeOldValue | 表示觀察 attributes 變動時,是否需要記錄變動前的屬性值 |
characterDataOldValue | 表示觀察 characterData 變動時,是否需要記錄變動前的值 |
attributeFilter | 表示需要觀察的特定屬性(例:['class','src']) |
屬性 | 說明 |
---|---|
type | 觀察的變動類型(attribute、characterData 或者 childList) |
target | 發生變動的 DOM 節點 |
addedNodes | 新增的 DOM 節點 |
removedNodes | 刪除的 DOM 節點 |
previousSibling | 前一個同級節點,如果沒有則返回 null |
nextSibling | 下一個同級節點,如果沒有則返回 null |
attributeName | 發生變動的屬性。如果設置了 attributeFilter,則只返回預先指定的屬性。 |
oldValue | 變動前的值。這個屬性只對 attribute 和 characterData 變動有效,如果發生 childList 變動,則返回 null |
你或許再開發一個所見即所得的服務,嘗試實踐【上一步】或【拖拉 DOM】等功能,我們可以利用 MutationObserver API,知道使用者進行了哪些修改,因此可以輕鬆地撤消這些動作。
DOM 操作是前端開發的基礎,掌握正確的操作方法和優化技巧對於建立高效能的 Web 非常重要。
最好的優化往往來自於理解瀏覽器的工作原理,然後據此調整你的程式碼。
不斷實踐這些技巧,並使用 Chrome DevTools 來驗證你的優化效果。隨著經驗的累積,你將能夠寫出更高效、更流暢的前端 Web。
在我的課程中就會有許多這樣的小知識點的教學,想一起學習更多 JavaScript
現在就加入 👉 https://thecodingpro.com/courses/javascript