渲染篇四:Event Loop 與異步更新策略

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钦讳、setImmediatescript(整體代碼)枕面、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 的世界一窺究竟凉倚。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市嫂沉,隨后出現(xiàn)的幾起案子稽寒,更是在濱河造成了極大的恐慌,老刑警劉巖趟章,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件杏糙,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡蚓土,警方通過查閱死者的電腦和手機(jī)宏侍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蜀漆,“玉大人谅河,你說我怎么就攤上這事。” “怎么了绷耍?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵吐限,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我褂始,道長(zhǎng)诸典,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任崎苗,我火速辦了婚禮狐粱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘益缠。我一直安慰自己脑奠,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布幅慌。 她就那樣靜靜地躺著宋欺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胰伍。 梳的紋絲不亂的頭發(fā)上齿诞,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音骂租,去河邊找鬼祷杈。 笑死,一個(gè)胖子當(dāng)著我的面吹牛渗饮,可吹牛的內(nèi)容都是我干的但汞。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼互站,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼私蕾!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起胡桃,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤踩叭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后翠胰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體容贝,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年之景,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了斤富。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡锻狗,死狀恐怖茂缚,靈堂內(nèi)的尸體忽然破棺而出戏罢,到底是詐尸還是另有隱情,我是刑警寧澤脚囊,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布龟糕,位于F島的核電站,受9級(jí)特大地震影響悔耘,放射性物質(zhì)發(fā)生泄漏讲岁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一衬以、第九天 我趴在偏房一處隱蔽的房頂上張望缓艳。 院中可真熱鬧,春花似錦看峻、人聲如沸阶淘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽溪窒。三九已至,卻和暖如春冯勉,著一層夾襖步出監(jiān)牢的瞬間澈蚌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來泰國打工灼狰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宛瞄,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓交胚,卻偏偏與公主長(zhǎng)得像份汗,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蝴簇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容