DOM 優(yōu)化原理與基本實踐
JS
是很快的锈遥,在 JS
中修改DOM
對象也是很快的。在JS
的世界里象迎,一切是簡單的凡橱、迅速的。但 DOM
操作并非 JS
一個人的獨舞台汇,而是兩個模塊之間的協(xié)作苛骨。
JS
引擎和渲染引擎(瀏覽器內(nèi)核)是獨立實現(xiàn)的篱瞎。當(dāng)我們用JS
去操作DOM
時,本質(zhì)上是JS
引擎和渲染引擎之間進行了跨界交流
痒芝。這個跨界交流
的實現(xiàn)并不簡單俐筋,它依賴了橋接接口作為橋梁
。
對 DOM 的修改引發(fā)樣式的更迭
我們對 DOM
的操作都不會局限于訪問严衬,而是為了修改它澄者。當(dāng)我們對DOM
的修改會引發(fā)它外觀(樣式)上的改變時,就會觸發(fā)回流或重繪请琳。
這個過程本質(zhì)上還是因為我們對DOM
的修改觸發(fā)了渲染樹(Render Tree
)的變化所導(dǎo)致的粱挡,重繪不一定導(dǎo)致回流,回流一定會導(dǎo)致重繪俄精。硬要比較的話询筏,回流比重繪做的事情更多,帶來的開銷也更大嘀倒。但這兩個說到底都是吃性能的屈留,所以都不是什么善茬。我們在開發(fā)中测蘑,要從代碼層面出發(fā)灌危,盡可能把回流和重繪的次數(shù)最小化。
DOM Fragment
DocumentFragment
接口表示一個沒有父級文件的最小文檔對象碳胳。它被當(dāng)做一個輕量版的Document
使用勇蝙,用于存儲已排好版的或尚未打理好格式的XML
片段。因為DocumentFragment
不是真實DOM
樹的一部分挨约,它的變化不會引起DOM
樹的重新渲染的操作(reflow
)味混,且不會導(dǎo)致性能等問題。作為脫離了真實DOM
樹的容器出現(xiàn)诫惭,用于緩存批量化的DOM
操作翁锡。
let container = document.getElementById('container')
// 創(chuàng)建一個DOM Fragment對象作為容器
let content = document.createDocumentFragment()
for(let count=0;count<10000;count++){
// span此時可以通過DOM API去創(chuàng)建
let oSpan = document.createElement("span")
oSpan.innerHTML = '我是一個小測試'
// 像操作真實DOM一樣操作DOM Fragment對象
content.appendChild(oSpan)
}
// 內(nèi)容處理好了,最后再觸發(fā)真實DOM的更改
container.appendChild(content)
我們運行這段代碼,可以得到與前面兩種寫法相同的運行結(jié)果夕土。
可以看出馆衔,DOM Fragment
對象允許我們像操作真實 DOM
一樣去調(diào)用各種各樣的DOM API
,我們的代碼質(zhì)量因此得到了保證怨绣。并且它的身份也非常純粹:當(dāng)我們試圖將其append
進真實DOM
時角溃,它會在乖乖交出自身緩存的所有后代節(jié)點后全身而退,完美地完成一個容器的使命篮撑,而不會出現(xiàn)在真實的 DOM
結(jié)構(gòu)中减细。這種結(jié)構(gòu)化、干凈利落的特性赢笨,使得DOM FragmentDOM Fragment
作為經(jīng)典的性能優(yōu)化手段大受歡迎未蝌。
Event Loop 與異步更新策略
Micro-Task 與 Macro-Task
事件循環(huán)中的異步隊列有兩種:macro
(宏任務(wù))隊列和 micro
(微任務(wù))隊列驮吱。
- 常見的
macro-task
比如:setTimeout
、setInterval
树埠、setImmediate
糠馆、script
(整體代碼)、I/O
操作怎憋、UI
渲染等又碌。 - 常見的
micro-task
比如:process.nextTick
、Promise
绊袋、MutationObserver
等毕匀。
Event Loop 過程解析
一個完整的 Event Loop
過程,可以概括為以下階段:
初始狀態(tài):調(diào)用棸┍穑空皂岔。
micro
隊列空,macro
隊列里有且只有一個script
腳本(整體代碼)展姐。全局上下文(
script
標(biāo)簽)被推入調(diào)用棧躁垛,同步代碼執(zhí)行。在執(zhí)行的過程中圾笨,通過對一些接口的調(diào)用教馆,可以產(chǎn)生新的macro-task
與micro-task
,它們會分別被推入各自的任務(wù)隊列里擂达。同步代碼執(zhí)行完了土铺,script
腳本會被移出macro
隊列,這個過程本質(zhì)上是隊列的macro-task
的執(zhí)行和出隊的過程板鬓。
- 上一步我們出隊的是一個
macro-task
悲敷,這一步我們處理的是micro-task
。但需要注意的是:當(dāng)macro-task
出隊時俭令,任務(wù)是一個一個執(zhí)行的后德;而micro-task
出隊時,任務(wù)是一隊一隊執(zhí)行的(如下圖所示)抄腔。因此探遵,我們處理micro
隊列這一步,會逐個執(zhí)行隊列中的任務(wù)并把它出隊妓柜,直到隊列被清空。
執(zhí)行渲染操作涯穷,更新界面(敲黑板劃重點)棍掐。
檢查是否存在
Web worker
任務(wù),如果有拷况,則對其進行處理 作煌。(上述過程循環(huán)往復(fù)掘殴,直到兩個隊列都清空)
異步更新策略
當(dāng)我們使用Vue
或React
提供的接口去更新數(shù)據(jù)時,這個更新并不會立即生效粟誓,而是會被推入到一個隊列里奏寨。待到適當(dāng)?shù)臅r機,隊列中的更新任務(wù)會被批量觸發(fā)鹰服。這就是異步更新病瞳。
最典型的例子,比如有時我們會遇到這樣的情況:
// 任務(wù)一
this.content = '第一次測試'
// 任務(wù)二
this.content = '第二次測試'
// 任務(wù)三
this.content = '第三次測試'
我們在三個更新任務(wù)中對同一個狀態(tài)修改了三次悲酷,如果我們采取傳統(tǒng)的同步更新策略套菜,那么就要操作三次DOM
。但本質(zhì)上需要呈現(xiàn)給用戶的目標(biāo)內(nèi)容其實只是第三次的結(jié)果设易,也就是說只有第三次的操作是有意義的——我們白白浪費了兩次計算逗柴。
但如果我們把這三個任務(wù)塞進異步更新隊列里,它們會先在
JS
的層面上被批量執(zhí)行完畢顿肺。當(dāng)流程走到渲染這一步時戏溺,它僅僅需要針對有意義的計算結(jié)果操作一次DOM
——這就是異步更新的妙處。