Vue 和 React 都實(shí)現(xiàn)了異步更新策略。雖然實(shí)現(xiàn)的方式不盡相同,但都達(dá)到了減少 DOM 操作睬棚、避免過度渲染的目的抠忘。通過研究框架的運(yùn)行機(jī)制,其設(shè)計(jì)思路將深化我們對(duì) DOM 優(yōu)化的理解结耀,其實(shí)現(xiàn)手法將拓寬我們對(duì) DOM 實(shí)踐的認(rèn)知留夜。
本節(jié)我們將基于 Event Loop 機(jī)制匙铡,對(duì) Vue 的異步更新策略作探討。
前置知識(shí):Event Loop 中的“渲染時(shí)機(jī)”
搞懂 Event Loop碍粥,是理解 Vue 對(duì) DOM 操作優(yōu)化的第一步鳖眼。
Micro-Task 與 Macro-Task
事件循環(huán)中的異步隊(duì)列有兩種:macro(宏任務(wù))隊(duì)列和 micro(微任務(wù))隊(duì)列。
常見的 macro-task 比如:
setTimeout
嚼摩、setInterval
钦讳、setImmediate
、script(整體代碼)
枕面、I/O 操作
愿卒、UI 渲染
等。
常見的 micro-task 比如:process.nextTick
潮秘、Promise
琼开、MutationObserver
等。
Event Loop 過程解析
基于對(duì) micro 和 macro 的認(rèn)知枕荞,我們來走一遍完整的事件循環(huán)過程柜候。
一個(gè)完整的 Event Loop 過程,可以概括為以下階段:
- 初始狀態(tài):調(diào)用楑锞空渣刷。micro 隊(duì)列空,macro 隊(duì)列里有且只有一個(gè) script 腳本(整體代碼)矗烛。
- 全局上下文(script 標(biāo)簽)被推入調(diào)用棧辅柴,同步代碼執(zhí)行。在執(zhí)行的過程中高诺,通過對(duì)一些接口的調(diào)用碌识,可以產(chǎn)生新的 macro-task 與 micro-task,它們會(huì)分別被推入各自的任務(wù)隊(duì)列里虱而。同步代碼執(zhí)行完了筏餐,script 腳本會(huì)被移出 macro 隊(duì)列,這個(gè)過程本質(zhì)上是隊(duì)列的 macro-task 的執(zhí)行和出隊(duì)的過程牡拇。
- 上一步我們出隊(duì)的是一個(gè) macro-task魁瞪,這一步我們處理的是 micro-task。但需要注意的是:
當(dāng) macro-task 出隊(duì)時(shí)惠呼,任務(wù)是**一個(gè)一個(gè)**執(zhí)行的导俘;而 micro-task 出隊(duì)時(shí),任務(wù)是**一隊(duì)一隊(duì)**執(zhí)行的
(如下圖所示)剔蹋。因此旅薄,我們處理 micro 隊(duì)列這一步,會(huì)逐個(gè)執(zhí)行隊(duì)列中的任務(wù)并把它出隊(duì),直到隊(duì)列被清空少梁。
SteveJobsBook.jpg- 執(zhí)行渲染操作洛口,更新界面(敲黑板劃重點(diǎn))。
- 檢查是否存在 Web worker 任務(wù)凯沪,如果有第焰,則對(duì)其進(jìn)行處理 。
(上述過程循環(huán)往復(fù)妨马,直到兩個(gè)隊(duì)列都清空)
我們總結(jié)一下挺举,每一次循環(huán)都是一個(gè)這樣的過程:
[圖片上傳失敗...(image-f2601f-1544542344708)]
渲染的時(shí)機(jī)
大家現(xiàn)在思考一個(gè)這樣的問題:假如我想要在異步任務(wù)里進(jìn)行DOM更新,我該把它包裝成 micro 還是 macro 呢烘跺?
我們先假設(shè)它是一個(gè) macro 任務(wù)湘纵,比如我在 script 腳本中用 setTimeout 來處理它:
// task是一個(gè)用于修改DOM的回調(diào)
setTimeout(task, 0)
現(xiàn)在 task 被推入的 macro 隊(duì)列。但因?yàn)?script 腳本本身是一個(gè) macro 任務(wù)滤淳,所以本次執(zhí)行完 script 腳本之后瞻佛,下一個(gè)步驟就要去處理 micro 隊(duì)列了,再往下就去執(zhí)行了一次 render娇钱,對(duì)不對(duì)?
但本次render我的目標(biāo)task其實(shí)并沒有執(zhí)行绊困,想要修改的DOM也沒有修改文搂,因此這一次的render其實(shí)是一次無效的render。
macro 不 ok秤朗,我們轉(zhuǎn)向 micro 試試看煤蹭。我用 Promise 來把 task 包裝成是一個(gè) micro 任務(wù):
Promise.resolve().then(task)
那么我們結(jié)束了對(duì) script 腳本的執(zhí)行,是不是緊接著就去處理 micro-task 隊(duì)列了取视?micro-task 處理完硝皂,DOM 修改好了,緊接著就可以走 render 流程了——不需要再消耗多余的一次渲染作谭,不需要再等待一輪事件循環(huán)稽物,直接為用戶呈現(xiàn)最即時(shí)的更新結(jié)果。
因此折欠,我們更新 DOM 的時(shí)間點(diǎn)贝或,應(yīng)該盡可能靠近渲染的時(shí)機(jī)。當(dāng)我們需要在異步任務(wù)中實(shí)現(xiàn) DOM 修改時(shí)锐秦,把它包裝成 micro 任務(wù)是相對(duì)明智的選擇咪奖。
生產(chǎn)實(shí)踐:異步更新策略——以 Vue 為例
什么是異步更新?
當(dāng)我們使用 Vue 或 React 提供的接口去更新數(shù)據(jù)時(shí)酱床,這個(gè)更新并不會(huì)立即生效羊赵,而是會(huì)被推入到一個(gè)隊(duì)列里。待到適當(dāng)?shù)臅r(shí)機(jī)扇谣,隊(duì)列中的更新任務(wù)會(huì)被批量觸發(fā)昧捷。這就是異步更新闲昭。
異步更新可以幫助我們避免過度渲染,是我們上節(jié)提到的“讓 JS 為 DOM 分壓”的典范之一料身。
異步更新的優(yōu)越性
異步更新的特性在于它只看結(jié)果汤纸,因此渲染引擎不需要為過程買單。
最典型的例子芹血,比如有時(shí)我們會(huì)遇到這樣的情況:
// 任務(wù)一
this.content = '第一次測(cè)試'
// 任務(wù)二
this.content = '第二次測(cè)試'
// 任務(wù)三
this.content = '第三次測(cè)試'
我們?cè)谌齻€(gè)更新任務(wù)中對(duì)同一個(gè)狀態(tài)修改了三次贮泞,如果我們采取傳統(tǒng)的同步更新策略,那么就要操作三次 DOM幔烛。但本質(zhì)上需要呈現(xiàn)給用戶的目標(biāo)內(nèi)容其實(shí)只是第三次的結(jié)果啃擦,也就是說只有第三次的操作是有意義的——我們白白浪費(fèi)了兩次計(jì)算。
但如果我們把這三個(gè)任務(wù)塞進(jìn)異步更新隊(duì)列里饿悬,它們會(huì)先在 JS 的層面上被批量執(zhí)行完畢令蛉。當(dāng)流程走到渲染這一步時(shí),它僅僅需要針對(duì)有意義的計(jì)算結(jié)果操作一次 DOM——這就是異步更新的妙處狡恬。
Vue狀態(tài)更新手法:nextTick
Vue 每次想要更新一個(gè)狀態(tài)的時(shí)候珠叔,會(huì)先把它這個(gè)更新操作給包裝成一個(gè)異步操作派發(fā)出去。這件事情弟劲,在源碼中是由一個(gè)叫做 nextTick 的函數(shù)來完成的:
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 檢查上一個(gè)異步任務(wù)隊(duì)列(即名為callbacks的任務(wù)數(shù)組)是否派發(fā)和執(zhí)行完畢了祷安。pending此處相當(dāng)于一個(gè)鎖
if (!pending) {
// 若上一個(gè)異步任務(wù)隊(duì)列已經(jīng)執(zhí)行完畢,則將pending設(shè)定為true(把鎖鎖上)
pending = true
// 是否要求一定要派發(fā)為macro任務(wù)
if (useMacroTask) {
macroTimerFunc()
} else {
// 如果不說明一定要macro 你們就全都是micro
microTimerFunc()
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
我們看到兔乞,Vue 的異步任務(wù)默認(rèn)情況下都是用 Promise 來包裝的汇鞭,也就是是說它們都是 micro-task。這一點(diǎn)和我們“前置知識(shí)”中的渲染時(shí)機(jī)的分析不謀而合庸追。
為了帶大家熟悉一下常見的 macro 和 micro 派發(fā)方式霍骄、加深對(duì) Event Loop 的理解,我們繼續(xù)細(xì)化解析一下 macroTimeFunc() 和 microTimeFunc() 兩個(gè)方法淡溯。
macroTimeFunc() 是這么實(shí)現(xiàn)的:
// macro首選setImmediate 這個(gè)兼容性最差
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
} else {
// 兼容性最好的派發(fā)方式是setTimeout
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
microTimeFunc() 是這么實(shí)現(xiàn)的:
// 簡(jiǎn)單粗暴 不是ios全都給我去Promise 如果不兼容promise 那么你只能將就一下變成macro了
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
p.then(flushCallbacks)
// in problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
} else {
// 如果無法派發(fā)micro读整,就退而求其次派發(fā)為macro
microTimerFunc = macroTimerFunc
}
我們注意到,無論是派發(fā) macro 任務(wù)還是派發(fā) micro 任務(wù)血筑,派發(fā)的任務(wù)對(duì)象都是一個(gè)叫做 flushCallbacks 的東西绘沉,這個(gè)東西做了什么呢?
flushCallbacks 源碼如下:
function flushCallbacks () {
pending = false
// callbacks在nextick中出現(xiàn)過 它是任務(wù)數(shù)組(隊(duì)列)
const copies = callbacks.slice(0)
callbacks.length = 0
// 將callbacks中的任務(wù)逐個(gè)取出執(zhí)行
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
現(xiàn)在我們理清楚了:Vue 中每產(chǎn)生一個(gè)狀態(tài)更新任務(wù)豺总,它就會(huì)被塞進(jìn)一個(gè)叫 callbacks 的數(shù)組(此處是任務(wù)隊(duì)列的實(shí)現(xiàn)形式)中车伞。這個(gè)任務(wù)隊(duì)列在被丟進(jìn) micro 或 macro 隊(duì)列之前,會(huì)先去檢查當(dāng)前是否有異步更新任務(wù)正在執(zhí)行(即檢查 pending 鎖)喻喳。如果確認(rèn) pending 鎖是開著的(false)另玖,就把它設(shè)置為鎖上(true),然后對(duì)當(dāng)前 callbacks 數(shù)組的任務(wù)進(jìn)行派發(fā)(丟進(jìn) micro 或 macro 隊(duì)列)和執(zhí)行。設(shè)置 pending 鎖的意義在于保證狀態(tài)更新任務(wù)的有序進(jìn)行谦去,避免發(fā)生混亂慷丽。
本小節(jié)我們從性能優(yōu)化的角度出發(fā),通過解析Vue源碼鳄哭,對(duì)異步更新這一高效的 DOM 優(yōu)化手段有了感性的認(rèn)知要糊。同時(shí)幫助大家進(jìn)一步熟悉了 micro 與 macro 在生產(chǎn)中的應(yīng)用,加深了對(duì) Event Loop 的理解妆丘。事實(shí)上锄俄,Vue 源碼中還有許多值得稱道的生產(chǎn)實(shí)踐,其設(shè)計(jì)模式與編碼細(xì)節(jié)都值得我們?nèi)ゼ?xì)細(xì)品味勺拣。對(duì)這個(gè)話題感興趣的同學(xué)奶赠,課后不妨移步 Vue運(yùn)行機(jī)制解析 進(jìn)行探索。
小結(jié)
至此药有,我們的 DOM 優(yōu)化之路才走完了一半毅戈。
以上我們都在討論“如何減少 DOM 操作”的話題。這個(gè)話題比較宏觀——DOM 操作也分很多種愤惰,它們帶來的變化各不相同苇经。有的操作只觸發(fā)重繪,這時(shí)我們的性能損耗就小一些宦言;有的操作會(huì)觸發(fā)回流塑陵,這時(shí)我們更“肉疼”一些。那么如何理解回流與重繪蜡励,如何借助這些理解去提升頁面渲染效率呢?
結(jié)束了 JS 的征程阻桅,我們下面就走進(jìn) CSS 的世界一窺究竟凉倚。