渲染篇 4:千方百計——Event Loop 與異步更新策略

千方百計——Event Loop與異步更新策略

Vue 和 React 都實現(xiàn)了異步更新策略臣咖。雖然實現(xiàn)的方式不盡相同跃捣,但都達到了減少 DOM 操作、避免過度渲染的目的夺蛇。通過研究框架的運行機制疚漆,其設(shè)計思路將深化我們對 DOM 優(yōu)化的理解,其實現(xiàn)手法將拓寬我們對 DOM 實踐的認知刁赦。

本節(jié)我們將基于 Event Loop 機制娶聘,對 Vue 的異步更新策略作探討。

前置知識:Event Loop 中的“渲染時機”

搞懂 Event Loop甚脉,是理解 Vue 對 DOM 操作優(yōu)化的第一步丸升。

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 等。


事件循環(huán)

Event Loop 過程解析

基于對 micro 和 macro 的認知,我們來走一遍完整的事件循環(huán)過程进倍。

一個完整的 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í)行的(如下圖1所示)。因此奴烙,我們處理 micro 隊列這一步助被,會逐個執(zhí)行隊列中的任務(wù)并把它出隊,直到隊列被清空切诀。

    圖1

  • 執(zhí)行渲染操作揩环,更新界面(敲黑板劃重點)。

  • 檢查是否存在 Web worker 任務(wù)幅虑,如果有丰滑,則對其進行處理 。

(上述過程循環(huán)往復(fù)倒庵,直到兩個隊列都清空)

我們總結(jié)一下褒墨,每一次循環(huán)都是一個這樣的過程:
圖2

渲染的時機

大家現(xiàn)在思考一個這樣的問題:假如我想要在異步任務(wù)里進行DOM更新,我該把它包裝成 micro 還是 macro 呢哄芜?

我們先假設(shè)它是一個 macro 任務(wù)貌亭,比如我在 script 腳本中用 setTimeout 來處理它:

// task是一個用于修改DOM的回調(diào)
setTimeout(task, 0)

現(xiàn)在 task 被推入的 macro 隊列柬唯。但因為 script 腳本本身是一個 macro 任務(wù)认臊,所以本次執(zhí)行完 script 腳本之后,下一個步驟就要去處理 micro 隊列了锄奢,再往下就去執(zhí)行了一次 render失晴,對不對剧腻?

但本次render我的目標(biāo)task其實并沒有執(zhí)行,想要修改的DOM也沒有修改涂屁,因此這一次的render其實是一次無效的render书在。

macro 不 ok,我們轉(zhuǎn)向 micro 試試看拆又。我用 Promise 來把 task 包裝成是一個 micro 任務(wù):

Promise.resolve().then(task)

那么我們結(jié)束了對 script 腳本的執(zhí)行儒旬,是不是緊接著就去處理 micro-task 隊列了?micro-task 處理完帖族,DOM 修改好了栈源,緊接著就可以走 render 流程了——不需要再消耗多余的一次渲染,不需要再等待一輪事件循環(huán)竖般,直接為用戶呈現(xiàn)最即時的更新結(jié)果甚垦。

因此,我們更新 DOM 的時間點涣雕,應(yīng)該盡可能靠近渲染的時機艰亮。當(dāng)我們需要在異步任務(wù)中實現(xiàn) DOM 修改時,把它包裝成 micro 任務(wù)是相對明智的選擇挣郭。

生產(chǎn)實踐:異步更新策略——以 Vue 為例

什么是異步更新迄埃?
當(dāng)我們使用 Vue 或 React 提供的接口去更新數(shù)據(jù)時,這個更新并不會立即生效兑障,而是會被推入到一個隊列里调俘。待到適當(dāng)?shù)臅r機,隊列中的更新任務(wù)會被批量觸發(fā)旺垒。這就是異步更新彩库。

異步更新可以幫助我們避免過度渲染,是我們上節(jié)提到的“讓 JS 為 DOM 分壓”的典范之一先蒋。

異步更新的優(yōu)越性

異步更新的特性在于它只看結(jié)果骇钦,因此渲染引擎不需要為過程買單

最典型的例子竞漾,比如有時我們會遇到這樣的情況:

// 任務(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——這就是異步更新的妙處。

Vue狀態(tài)更新手法:nextTick

Vue 每次想要更新一個狀態(tài)的時候借笙,會先把它這個更新操作給包裝成一個異步操作派發(fā)出去扒怖。這件事情,在源碼中是由一個叫做 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)
    }
  })
  // 檢查上一個異步任務(wù)隊列(即名為callbacks的任務(wù)數(shù)組)是否派發(fā)和執(zhí)行完畢了业稼。pending此處相當(dāng)于一個鎖
  if (!pending) {
    // 若上一個異步任務(wù)隊列已經(jīng)執(zhí)行完畢盗痒,則將pending設(shè)定為true(把鎖鎖上)
    pending = true
    // 是否要求一定要派發(fā)為macro任務(wù)
    if (useMacroTask) {
      macroTimerFunc()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

我們看到,Vue 的異步任務(wù)默認情況下都是用 Promise 來包裝的低散,也就是是說它們都是 micro-task俯邓。這一點和我們“前置知識”中的渲染時機的分析不謀而合。

為了帶大家熟悉一下常見的 macro 和 micro 派發(fā)方式熔号、加深對 Event Loop 的理解看成,我們繼續(xù)細化解析一下 macroTimeFunc() 和 microTimeFunc() 兩個方法。

macroTimeFunc() 是這么實現(xiàn)的:

// macro首選setTmmediate 這個兼容性最差
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() 是這么實現(xiàn)的:

// 簡單粗暴 不是ios全都給我去Promise 如果不兼容promise 那么你只能將就一下變成macro了
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
    // 在有問題的uiwebview中跨嘉,promise.then不會完全中斷川慌,但它可能會陷入一種奇怪的狀態(tài),
    // 即回調(diào)被推到微任務(wù)隊列中祠乃,但隊列不會被刷新梦重,直到瀏覽器需要做一些其他工作,例如處
    // 理計時器亮瓷。因此琴拧,我們可以通過添加一個空計時器來“強制”刷新微任務(wù)隊列。
    if (isIOS) setTimeout(noop)
  }
} else {
  // 如果無法派發(fā)micro嘱支,就退而求其次派發(fā)為macro
  microTimerFunc = macroTimerFunc
}

我們注意到蚓胸,無論是派發(fā) macro 任務(wù)還是派發(fā) micro 任務(wù),派發(fā)的任務(wù)對象都是一個叫做 flushCallbacks 的東西除师,這個東西做了什么呢沛膳?

flushCallbacks 源碼如下:

function flushCallbacks () {
 pending = false
  // callbacks在nextick中出現(xiàn)過 它是任務(wù)數(shù)組(隊列)
  const copies = callbacks.slice(0)
  callbacks.length = 0
  // 將callbacks中的任務(wù)逐個取出執(zhí)行
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

現(xiàn)在我們理清楚了:Vue 中每產(chǎn)生一個狀態(tài)更新任務(wù),它就會被塞進一個叫 callbacks 的數(shù)組(此處是任務(wù)隊列的實現(xiàn)形式)中汛聚。這個任務(wù)隊列在被丟進 micro 或 macro 隊列之前锹安,會先去檢查當(dāng)前是否有異步更新任務(wù)正在執(zhí)行(即檢查 pending 鎖)。如果確認 pending 鎖是開著的(false)倚舀,就把它設(shè)置為鎖上(true)叹哭,然后對當(dāng)前 callbacks 數(shù)組的任務(wù)進行派發(fā)(丟進 micro 或 macro 隊列)和執(zhí)行。設(shè)置 pending 鎖的意義在于保證狀態(tài)更新任務(wù)的有序進行痕貌,避免發(fā)生混亂风罩。

本小節(jié)我們從性能優(yōu)化的角度出發(fā),通過解析Vue源碼舵稠,對異步更新這一高效的 DOM 優(yōu)化手段有了感性的認知超升。同時幫助大家進一步熟悉了 micro 與 macro 在生產(chǎn)中的應(yīng)用入宦,加深了對 Event Loop 的理解。事實上廓俭,Vue 源碼中還有許多值得稱道的生產(chǎn)實踐,其設(shè)計模式與編碼細節(jié)都值得我們?nèi)ゼ毤毱肺栋ぁ@個話題感興趣的同學(xué)研乒,課后不妨移步 Vue運行機制解析 進行探索。

小結(jié)

至此淋硝,我們的 DOM 優(yōu)化之路才走完了一半雹熬。

以上我們都在討論“如何減少 DOM 操作”的話題。這個話題比較宏觀——DOM 操作也分很多種谣膳,它們帶來的變化各不相同竿报。有的操作只觸發(fā)重繪,這時我們的性能損耗就小一些继谚;有的操作會觸發(fā)回流烈菌,這時我們更“肉疼”一些。那么如何理解回流與重繪花履,如何借助這些理解去提升頁面渲染效率呢芽世?

結(jié)束了 JS 的征程,我們下面就走進 CSS 的世界一窺究竟诡壁。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末济瓢,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子妹卿,更是在濱河造成了極大的恐慌旺矾,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件夺克,死亡現(xiàn)場離奇詭異箕宙,居然都是意外死亡,警方通過查閱死者的電腦和手機铺纽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進店門扒吁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人室囊,你說我怎么就攤上這事雕崩。” “怎么了融撞?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵盼铁,是天一觀的道長。 經(jīng)常有香客問我尝偎,道長饶火,這世上最難降的妖魔是什么鹏控? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮肤寝,結(jié)果婚禮上当辐,老公的妹妹穿的比我還像新娘。我一直安慰自己鲤看,他們只是感情好缘揪,可當(dāng)我...
    茶點故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著义桂,像睡著了一般找筝。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上慷吊,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天袖裕,我揣著相機與錄音,去河邊找鬼溉瓶。 笑死急鳄,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的堰酿。 我是一名探鬼主播攒岛,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼胞锰!你這毒婦竟也來了灾锯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤嗅榕,失蹤者是張志新(化名)和其女友劉穎顺饮,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體凌那,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡兼雄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了帽蝶。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赦肋。...
    茶點故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖励稳,靈堂內(nèi)的尸體忽然破棺而出佃乘,到底是詐尸還是另有隱情,我是刑警寧澤驹尼,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布趣避,位于F島的核電站,受9級特大地震影響新翎,放射性物質(zhì)發(fā)生泄漏程帕。R本人自食惡果不足惜住练,卻給世界環(huán)境...
    茶點故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望愁拭。 院中可真熱鬧讲逛,春花似錦、人聲如沸岭埠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽枫攀。三九已至括饶,卻和暖如春株茶,著一層夾襖步出監(jiān)牢的瞬間来涨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工启盛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蹦掐,地道東北人。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓僵闯,卻偏偏與公主長得像卧抗,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鳖粟,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,955評論 2 355

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