App下載

你也可以理解的React Fiber,學(xué)廢了嗎

猿友 2020-09-24 14:57:09 瀏覽數(shù) (5044)
反饋

文章來(lái)源于公眾號(hào):前端時(shí)光屋 作者:小豪

Fiber出現(xiàn)的背景?

在早期的 React 版本中,也就是 React16.8 版本之前。

大量的同步計(jì)算任務(wù)阻塞了瀏覽器的UI渲染。默認(rèn)情況下,JS運(yùn)算、頁(yè)面布局頁(yè)面繪制渲染都是運(yùn)行在瀏覽器的主線程當(dāng)中,他們之間是互斥的關(guān)系。

如果 JS 運(yùn)算持續(xù)占用主線程,頁(yè)面就沒(méi)法得到及時(shí)的更新,當(dāng)我們調(diào)用setState更新頁(yè)面的時(shí)候,React 會(huì)遍歷應(yīng)用的所有節(jié)點(diǎn),與老的 dom 節(jié)點(diǎn)進(jìn)行 diff 算法的對(duì)比,最小代價(jià)更新頁(yè)面,即使這樣,整個(gè)過(guò)程也是一氣呵成,不能被打斷的,如果頁(yè)面元素很多,整個(gè)過(guò)程占用的時(shí)間就可能超過(guò)16毫秒,出現(xiàn)掉幀的現(xiàn)象。

針對(duì)這一現(xiàn)象,React 團(tuán)隊(duì)從框架層面對(duì) web 頁(yè)面的運(yùn)行機(jī)制做了優(yōu)化,此后,Fiber誕生了。

說(shuō)到16ms,我們來(lái)看這樣的一個(gè)概念

屏幕刷新率

  • 目前大多數(shù)設(shè)備的屏幕刷新率為60次/秒
  • 瀏覽器的渲染動(dòng)畫或頁(yè)面的每一幀的速率也需要跟設(shè)備屏幕的刷新率保持一致。
  • 頁(yè)面是一幀一幀繪制出來(lái)的,當(dāng)每秒繪制的幀數(shù)(FPS)達(dá)到60時(shí),頁(yè)面是流暢的,小于這個(gè)值時(shí),用戶會(huì)感覺到卡頓。
  • 每個(gè)幀的預(yù)算時(shí)間是16.66毫秒(1秒/60)
  • 1s 60幀,所以我們書寫代碼時(shí)盡量不讓一幀的工作量超過(guò)16ms

Fiber的誕生

解決主線程長(zhǎng)時(shí)間被 JS 暈眩占用這一問(wèn)題的基本思路,是將運(yùn)算切割為多個(gè)步驟,分批完成。也就是說(shuō)在完成一部分任務(wù)之后, 將控制權(quán)交回給瀏覽器,讓瀏覽器有時(shí)間再進(jìn)行頁(yè)面的渲染。等瀏覽器忙完之后,再繼續(xù)之前React未完成的任務(wù)。

舊版 React 通過(guò)遞歸的方式進(jìn)行渲染,使用的是 JS 引擎自身的函數(shù)調(diào)用棧,它會(huì)一直執(zhí)行到??諡橹?/strong>。

Fiber實(shí)現(xiàn)了自己的組件調(diào)用棧,它以鏈表的形式遍歷組件樹,可以靈活地暫停、繼續(xù)和丟棄執(zhí)行的任務(wù)。實(shí)現(xiàn)的方式是使用了 瀏覽器的requestIdleCallback這一 API。官方的解釋是這樣的:

window.requestIdleCallback()會(huì)在瀏覽器空閑時(shí)期依次調(diào)用函數(shù),這就可以讓開發(fā)者在主事件循環(huán)中執(zhí)行后臺(tái)優(yōu)先級(jí)低的任務(wù),而且不會(huì)像對(duì)動(dòng)畫和用戶交互這些延遲觸發(fā)產(chǎn)生關(guān)鍵的事件影響。函數(shù)一般會(huì)按先進(jìn)先調(diào)用的順序執(zhí)行,除非函數(shù)在瀏覽器調(diào)用它之前就到了它的超時(shí)時(shí)間。

requestIdleCallback的核心用法

  • 希望快速響應(yīng)用戶,讓用戶覺得夠快,不能阻塞用戶的交互行為
  • requestIdleCallback 使開發(fā)者能夠在主事件循環(huán)上執(zhí)行后臺(tái)和低優(yōu)先級(jí)的工作,而不會(huì)影響延遲關(guān)鍵事件,例如動(dòng)畫和輸入的響應(yīng)
  • 正常幀任務(wù)完成后沒(méi)超過(guò)16ms,說(shuō)明時(shí)間有賦予,此時(shí)就會(huì)執(zhí)行requestIdleCallback里注冊(cè)的任務(wù)

requestIdleCallback執(zhí)行流程

requestIdleCallback執(zhí)行流程

Fiber是什么

Fiber是一個(gè)執(zhí)行單元

Fiber 是一個(gè)執(zhí)行單元,每次執(zhí)行完一個(gè)執(zhí)行單元, React 就會(huì)檢查現(xiàn)在還剩多少時(shí)間,如果沒(méi)有時(shí)間就將控制權(quán)讓出去

Fiber是一個(gè)執(zhí)行單元

Fiber是一種數(shù)據(jù)結(jié)構(gòu)

React 目前的做法是使用鏈表, 每個(gè) VirtualDOM 節(jié)點(diǎn)內(nèi)部表示為一個(gè)Fiber,它可以用一個(gè) JS 對(duì)象來(lái)表示:

const fiber = {
  stateNode, // 節(jié)點(diǎn)實(shí)例
  child,     // 子節(jié)點(diǎn)
  sibling,   // 兄弟節(jié)點(diǎn)
  return,    // 父節(jié)點(diǎn)
}

Fiber是一種數(shù)據(jù)結(jié)構(gòu)

Fiber之前的協(xié)調(diào)階段

  • React 會(huì)遞歸比對(duì)VirtualDOM樹,找出需要變動(dòng)的節(jié)點(diǎn),然后同步更新它們。這個(gè)過(guò)程 React 稱為Reconcilation(協(xié)調(diào))
  • 在Reconcilation期間,React 會(huì)一直占用著瀏覽器資源,一則會(huì)導(dǎo)致用戶觸發(fā)的事件得不到響應(yīng), 二則會(huì)導(dǎo)致掉幀,用戶可能會(huì)感覺到卡頓

let root = {
  key: 'A1',
  children: [
    {
      key: 'B1',
      children: [
        {
          key: 'C1',
          children: []
        },
        {
          key: 'C2',
          children: []
        }
      ]
    },
    {
      key: 'B2',
      children: []
    }
  ]
}


function walk(element) {
  doWork(element);
  element.children.forEach(walk);
}


function doWork(element) {
  console.log(element.key);
}
walk(root);

在 Fiber 出現(xiàn)之前, React 會(huì)不斷遞歸遍歷虛擬 DOM 節(jié)點(diǎn),占用著瀏覽器資源,積極地浪費(fèi)性能,造成卡頓現(xiàn)象,且協(xié)調(diào)階段是不能被打斷的

React不斷遞歸遍歷虛擬DOM節(jié)點(diǎn)

Fiber 出現(xiàn)之后,通過(guò)某些 Fiber 調(diào)度策略合理分配 CPU 資源,讓自己的協(xié)調(diào)階段變成可被終端,適時(shí)地讓 CPU(瀏覽器)執(zhí)行權(quán),提高了性能優(yōu)化。

協(xié)調(diào)階段變成可被終端

Fiber執(zhí)行階段

每次渲染有兩個(gè)階段:Reconciliation(協(xié)調(diào)\render階段)和Commit(提交階段)

  • 協(xié)調(diào)階段: 這個(gè)階段可以被中斷, 通過(guò)Dom-Diff算法找出所有節(jié)點(diǎn)變更,例如節(jié)點(diǎn)新增、刪除屬性變更等等, 這些變更React 稱之為副作用(Effect)
  • 提交階段: 將上一個(gè)階段計(jì)算出來(lái)的需要處理的副作用(Effects)一次性執(zhí)行了。這個(gè)階段必須同步執(zhí)行,不能被打斷

簡(jiǎn)單理解的話

  • 階段1:生成Fiber樹,得出需要更新節(jié)點(diǎn)信息。(可打斷
  • 階段2:將需要更新的節(jié)點(diǎn)一次性地批量更新。(不可打斷

Fiber的協(xié)調(diào)階段,可以被優(yōu)先級(jí)較高的任務(wù)(如鍵盤輸入)打斷。

階段1可被打斷的特性,讓優(yōu)先級(jí)更高的任務(wù)先執(zhí)行,從框架層面大大降低了頁(yè)面掉幀的概率。

Fiber執(zhí)行流程

render階段

Fiber Reconciliation(協(xié)調(diào)) 在階段一進(jìn)行 Diff 計(jì)算的時(shí)候,會(huì)生成一棵 Fiber 樹。這棵樹是在 Virtual DOM 樹的基礎(chǔ)上增加額外的信息生成來(lái)的,它本質(zhì)來(lái)說(shuō)是一個(gè)鏈表。

render階段

commit提交階段

Fiber 樹在首次渲染的時(shí)候會(huì)一次過(guò)生成。在后續(xù)需要 Diff 的時(shí)候,會(huì)根據(jù)已有樹和最新 Virtual DOM 的信息,生成一棵新的樹。這顆新樹每生成一個(gè)新的節(jié)點(diǎn),都會(huì)將控制權(quán)交回給主線程,去檢查有沒(méi)有優(yōu)先級(jí)更高的任務(wù)需要執(zhí)行。如果沒(méi)有,則繼續(xù)構(gòu)建樹的過(guò)程。

1.如果過(guò)程中有優(yōu)先級(jí)更高的任務(wù)需要進(jìn)行,則 Fiber Reconciler 會(huì)丟棄正在生成的樹,在空閑的時(shí)候再重新執(zhí)行一遍。

2.在構(gòu)造 Fiber 樹的過(guò)程中,F(xiàn)iber Reconciler 會(huì)將需要更新的節(jié)點(diǎn)信息保存在Effect List當(dāng)中,在階段二執(zhí)行的時(shí)候,會(huì)批量更新相應(yīng)的節(jié)點(diǎn)。

細(xì)節(jié)拓展

render階段是如何遍歷,生成Fiber樹的?

<div>

  

    

      

      

    

    

  
</div>

  • 從頂點(diǎn)開始遍歷
  • 如果有第一個(gè)兒子,先遍歷第一個(gè)兒子
  • 如果沒(méi)有第一個(gè)兒子,標(biāo)志著此節(jié)點(diǎn)遍歷完成
  • 如果有弟弟遍歷弟弟
  • 如果有沒(méi)有下一個(gè)弟弟,返回父節(jié)點(diǎn)標(biāo)識(shí)完成父節(jié)點(diǎn)遍歷,如果有叔叔遍歷叔叔
  • 沒(méi)有父節(jié)點(diǎn)遍歷結(jié)束

render節(jié)點(diǎn)遍歷規(guī)則

commit階段,是如何commit的?

類比 Git 分支功能,從舊樹中 fork 出來(lái)一份,在新分支進(jìn)行添加、刪除和更新操作,經(jīng)過(guò)測(cè)試后進(jìn)行提交。

commit階段,是如何commit的

以上就是W3Cschool編程獅關(guān)于你也可以理解的React Fiber,學(xué)廢了嗎的相關(guān)介紹了,希望對(duì)大家有所幫助。

0 人點(diǎn)贊