轉(zhuǎn):https://www.qiyuandi.com/zhanzhang/zonghe/16899.html
如果各位同學(xué)想系統(tǒng)的了解 Fiber,我個(gè)人還是強(qiáng)烈推薦 React 團(tuán)隊(duì) Lin Clark 在 youtube 上的視頻,這個(gè)是理解 React Fiber 最好的一手資源空民。
這篇文章會(huì)努力從一個(gè)不一樣的風(fēng)格去講述 Fiber泛豪,把這個(gè)復(fù)雜的系統(tǒng)通過(guò)最簡(jiǎn)單最直接的語(yǔ)言表達(dá)出來(lái)。但是鸯绿,在看本文之前,我希望各位讀者能對(duì) React 15 的 diff 算法和虛擬 dom 有比較基本的了解。
為什么要學(xué)習(xí) Fiber
對(duì)于一個(gè)前端工程師而言琴昆,無(wú)論從功利的角度還是非功利的角度,你都有足夠多的動(dòng)機(jī)學(xué)習(xí) Fiber馆揉。從功利的角度而言:
- Fiber 被高級(jí)別前端工程師職位頻繁考察
- 是否了解 Fiber 是互聯(lián)網(wǎng)公司考察程序員是否有深入理解某一事物意愿的重要指標(biāo)之一
從非功利的角度而言: Fiber 可以幫你把前端崗位的三個(gè)重要知識(shí)系統(tǒng)化,體系化:
- 瀏覽器渲染機(jī)制
- Js 運(yùn)行在單線(xiàn)程上
- 前端性能優(yōu)化
為什么要有 Fiber
一言以蔽之抖拦,F(xiàn)iber 是用來(lái)優(yōu)化 React 性能的升酣。首先,我們先來(lái)看看 React15 和 React16 的對(duì)比态罪。
React 15
[圖片上傳失敗...(image-2ad861-1660720729968)]
React 16
[圖片上傳失敗...(image-9ebcca-1660720729968)]
在深入理解其原理之前噩茄,我們有必要在這里做一些知識(shí)鋪墊:我們都知道,js 是一門(mén)單線(xiàn)程語(yǔ)言复颈,準(zhǔn)確的說(shuō)绩聘,是 js 運(yùn)行在瀏覽器渲染進(jìn)程 (render process) 的主線(xiàn)程上 (main thread)。但是這個(gè)主線(xiàn)程不僅僅是為 js 服務(wù)耗啦,它還負(fù)責(zé)其他的事情凿菩,比如響應(yīng)用戶(hù)的輸入,以及頁(yè)面的渲染帜讲,一個(gè)人做三件事衅谷,每?jī)蓚€(gè)事都不能并行,這就產(chǎn)生了一個(gè)問(wèn)題似将,假如說(shuō)任何一件事?lián)屨贾骶€(xiàn)程過(guò)多的時(shí)間获黔,另外兩個(gè)事就只能等著蚀苛。換句話(huà)說(shuō),假如有一個(gè)計(jì)算密集型 (cpu密集型) 的 js 任務(wù)霸占著主線(xiàn)程玷氏,那么瀏覽器是一定沒(méi)有時(shí)間響應(yīng)用戶(hù)輸入以及渲染頁(yè)面的堵未,那么這個(gè)東西就會(huì)導(dǎo)致你的頁(yè)面性能不好,也就是盏触,卡渗蟹。 不幸的是,對(duì)于 react 15 而言耻陕,當(dāng)你使用 react 并且有一棵非常龐大的虛擬 dom 樹(shù)的時(shí)候拙徽,你的 diff 算法就是這樣一個(gè)連續(xù)不可被打斷的計(jì)算密集型 js 任務(wù)。
Fiber 是如何優(yōu)化的性能
stack reconciler 面臨的難題
瀏覽器渲染進(jìn)程有一個(gè)主線(xiàn)程诗宣,這個(gè)主線(xiàn)程既負(fù)責(zé)運(yùn)行 js膘怕,也負(fù)責(zé)頁(yè)面渲染(布局,繪制召庞,合并圖層)岛心。所以你要么運(yùn)行 js,要么渲染頁(yè)面篮灼,不能兩個(gè)同時(shí)進(jìn)行忘古。在 React 15 中,假如一棵虛擬 dom 樹(shù)很大很大诅诱,你進(jìn)行 diff 的時(shí)候就要有大量的 js 運(yùn)算髓堪,這種 cpu 密集型的操作會(huì)占住主線(xiàn)程不放,這時(shí)你的頁(yè)面就會(huì)卡娘荡,用戶(hù)的輸入也得不到響應(yīng)干旁。
[圖片上傳失敗...(image-64d7f3-1660720729968)]
Fiber 扮演的角色
fiber 就好比給 react 加了一個(gè)操作系統(tǒng)。 告訴它什么時(shí)候 diff炮沐,什么時(shí)候渲染争群,什么時(shí)候響應(yīng)用戶(hù)輸入。這好比操作系統(tǒng)的時(shí)間片輪轉(zhuǎn)法大年,也很像 generator 允許中斷這種機(jī)制换薄。
由于有了 Fiber,React 在每一幀當(dāng)中都有時(shí)間響應(yīng)用戶(hù)輸入和進(jìn)行頁(yè)面渲染翔试,因此看起來(lái)不卡轻要。
[圖片上傳失敗...(image-a1c287-1660720729968)]
Fiber Reconciler 的全過(guò)程
知識(shí)鋪墊
瀏覽器的一幀
[圖片上傳失敗...(image-14ef0f-1660720729968)]
這篇文章對(duì)瀏覽器原理不會(huì)有詳盡的展開(kāi),但是垦缅,一些必要的知識(shí)背景對(duì)理解 Fiber 事至關(guān)重要的伦腐。我們可以看到,瀏覽器渲染進(jìn)程 Renderer Process 的主線(xiàn)程 Main Thread 在一幀內(nèi)會(huì)有很多步驟失都,解析 HTML 成 dom 樹(shù)柏蘑,計(jì)算并往 dom 樹(shù)里面嵌入樣式幸冻,計(jì)算布局(各個(gè) dom 節(jié)點(diǎn)在屏幕的什么位置),生成操作系統(tǒng)層面的繪制命令咳焚,為每一個(gè)圖層的繪制生成順序并生成繪制命令的序列洽损,然后進(jìn)行圖層的合成(注意,這一步不一定是主線(xiàn)程做革半,有可能是合成幀線(xiàn)程做的)碑定。如果主線(xiàn)程在一幀內(nèi)還有額外的時(shí)間,那么又官,它會(huì)去執(zhí)行 requestIdleCallback 里面的回調(diào)函數(shù)延刘。
requestIdleCallback
requestIdleCallback 這個(gè) API 是 Fiber 中非常重要的一個(gè)概念。我們已經(jīng)知道瀏覽器渲染進(jìn)程的主線(xiàn)程負(fù)責(zé)很多件事六敬,下圖中所有的事情都是主線(xiàn)程來(lái)完成的碘赖,如果主線(xiàn)程還有時(shí)間,它會(huì)去執(zhí)行 requestIdleCallback 里面的任務(wù)外构。如果沒(méi)有時(shí)間普泡,requestIdleCallback 里面的回調(diào)會(huì)被延遲到下一幀執(zhí)行,如果下一幀還沒(méi)時(shí)間审编,就繼續(xù)再往后推撼班。
[圖片上傳失敗...(image-2376d7-1660720729968)]
我們可以和 requestAmimationFrame 這個(gè)函數(shù)做一個(gè)對(duì)比實(shí)驗(yàn)。實(shí)驗(yàn)的目的是驗(yàn)證 rIC 并不是每一幀都會(huì)被執(zhí)行垒酬。
num = 300
function f() {
console.log('print')
num -= 1
if (num > 0)
requestIdleCallback(f) // 換成 requestAmimationFrame 試試
}
f()
Js
復(fù)制
你會(huì)發(fā)現(xiàn)砰嘁,對(duì)于 rAF,程序會(huì)在 5s 后停止勘究,但對(duì)于 rIC般码,程序的運(yùn)行時(shí)間遠(yuǎn)遠(yuǎn)大于 5s。
react 會(huì)使用 requestIdleCallback 來(lái)逐個(gè)更新 Fiber Node 節(jié)點(diǎn)乱顾,如果瀏覽器沒(méi)時(shí)間留給 requestIdleCallback,那么更新過(guò)程會(huì)被暫停宫静,讓出主線(xiàn)程走净。
更新流程概述
從 diff 到更新真實(shí) dom,這個(gè)過(guò)程可以被分成 4 步孤里。
- 生成 UpdateQueue
- 更新 WIP Tree
- 生成 EffectList
- 更新真實(shí) dom
前三個(gè)階段并成為 render 階段伏伯,最后一個(gè)階段為 commit 階段。大家可以先思考一個(gè)問(wèn)題捌袜,對(duì)于 Fiber 算法而言说搅,哪一個(gè)階段可以使用 requestIdleCallback,也就是可以被打斷虏等?
[圖片上傳失敗...(image-d769c3-1660720729968)]
詳盡的更新流程
代碼
import React, { Component, PureComponent } from 'react';
class List extends Component {
constructor(props) {
super(props);
this.state = {
list:[1,2,3]
}
}
render() {
const {list}=this.state
return ( <div>
<button onClick={()=>{
let {list}=this.state
for(let i=0;i<list.length;i++){
list[i]*=list[i]
}
this.setState({
list
})
}}>^2</button>
<Item num={list[0]} key={0}/>
<Item num={list[1]} key={1}/>
<Item num={list[2]} key={2}/>
</div> );
}
}
export default List;
Js
復(fù)制
class Item extends PureComponent {
constructor(props) {
super(props);
}
render() {
return (<div>
{this.props.num}
</div> );
}
}
export default Item;
Js
復(fù)制
代碼概述:
父組件
- 組件名:List
- state:
list = [1,2,3]
- props:無(wú)
子組件:
- 組件名:Item
- state:無(wú)
- props:
num = this.state.list[i]
[圖片上傳失敗...(image-6e2b03-1660720729967)]
List 組件的 setState
react 會(huì)把 setState 以某種數(shù)據(jù)結(jié)構(gòu)注入到 updateQueue 里面
[圖片上傳失敗...(image-3b6f17-1660720729967)]
當(dāng)我們點(diǎn)擊 button 觸發(fā) setState 以后弄唧,[1,2,3] 分別乘上自己變成 [1,4,9]适肠。
Fiber 樹(shù)
在 List 組件第一次渲染的時(shí)候,react 會(huì)用 jsx 生成好一棵 Fiber 樹(shù)放在內(nèi)存里面候引,這個(gè) Fiber 樹(shù)長(zhǎng)什么樣侯养?長(zhǎng)下面這樣:
[圖片上傳失敗...(image-826609-1660720729967)]
了解過(guò) React 15 的同學(xué)應(yīng)該知道,這個(gè)數(shù)據(jù)結(jié)構(gòu)和虛擬 dom 樹(shù)并沒(méi)有本質(zhì)的區(qū)別澄干,它最重要的區(qū)別就是它的指針變得更多了逛揩,但本質(zhì)上還是一個(gè)樹(shù)形結(jié)構(gòu)。至此麸俘,我們得到 Fiber 結(jié)構(gòu)的第一條信息:Fiber 是一棵鏈表樹(shù)辩稽。
Fiber 里面的每一個(gè)節(jié)點(diǎn)有指向它兒子,父節(jié)點(diǎn)和兄弟節(jié)點(diǎn)的指針从媚。
[圖片上傳失敗...(image-569f9a-1660720729967)]
為什么是鏈表樹(shù)
遞歸不好保護(hù)現(xiàn)場(chǎng)逞泄。事實(shí)上,react 團(tuán)隊(duì)也用 es6 generator 做過(guò)一些嘗試静檬,最后以失敗告終炭懊。結(jié)果證明,鏈表這種數(shù)據(jù)結(jié)構(gòu)是最好做中斷恢復(fù)的拂檩,這是因?yàn)殒湵碓谘h(huán)過(guò)程中侮腹,容易停止并保護(hù)現(xiàn)場(chǎng),讓主線(xiàn)程去響應(yīng)用戶(hù)輸入稻励,去做頁(yè)面渲染父阻。
Work In Progress 樹(shù)
react 會(huì)根據(jù) current tree 和 update queue 生成 work in progress tree。你可以簡(jiǎn)單的把 current tree 和 work in progress tree 理解為一次 setState 前后虛擬 dom 的 snapshot望抽。
React 會(huì)從觸發(fā) setState 的組件開(kāi)始更新加矛,由于 List 的 state 被改變,List 節(jié)點(diǎn)對(duì)應(yīng)的 effectTag 會(huì)置為 update煤篙,effectList 會(huì)生成一個(gè)鏈表節(jié)點(diǎn)(為了方便斟览,我這里用數(shù)組表示,大家知道 react 實(shí)際上用的是鏈表就可以了)
[圖片上傳失敗...(image-2b754a-1660720729967)]
[{
instance: List
type: ‘update’
property: ‘state.list’
value: [1,4,9]
}]
Js
復(fù)制
button 是 List 中的元素辑奈,但是它并沒(méi)有任何屬性的更新苛茂,WIP Tree 直接從 current Tree 復(fù)制節(jié)點(diǎn)。
第一個(gè) Item 由于 props.num 并沒(méi)有發(fā)生改變鸠窗,shouldComponentUpdate 里面的邏輯可以讓該組件不去觸發(fā)更新妓羊,Item 節(jié)點(diǎn)直接從 current tree 復(fù)制過(guò)來(lái)就可以了。
[圖片上傳失敗...(image-2d5eea-1660720729967)]
由于第一個(gè) Item 組件不需要觸發(fā)更新稍计,那么其子節(jié)點(diǎn)可以完全從 current Tree 復(fù)制過(guò)來(lái)躁绸。若 rIC 時(shí)間片未到期,React 會(huì)繼續(xù)更新 FiberNode。此時(shí)净刮,React 發(fā)現(xiàn)剥哑,第二個(gè) Item 的父元素的 state 發(fā)生了改變,也就是 Item 的 props 發(fā)生了改變庭瑰,同時(shí)星持,Item 里面 div 元素的 innerText 屬性發(fā)生了改變,因此第二個(gè) Item 的 FiberNode 的 effectList 變成
[{
instance: Item,
type: ‘update’,
property: ‘props.num’
value: 4
}]
Js
復(fù)制
div 的 FiberNode 對(duì)應(yīng)的 effectList 變?yōu)?/p>
[{
name: div,
type: ‘update’,
property: ‘innerText’
value: 4
}]
Js
復(fù)制
[圖片上傳失敗...(image-26ed27-1660720729967)]
被打斷
如果此時(shí) requestIdleCallback 的時(shí)間片到期弹灭,那么 react 會(huì)把控制權(quán)交給瀏覽器督暂,瀏覽器此時(shí)去響應(yīng)用戶(hù)輸入,渲染頁(yè)面穷吮。
下一幀逻翁,React 再次在 rIC 拿到了控制權(quán),它繼續(xù)更新第三個(gè) Item捡鱼,查看父元素的 state 后得知自己的 props 有了更新八回,Item 的 effectList 變成
[{
instance: Item,
type: ‘update’,
property: ‘props.num’
value: 9
}]
Js
復(fù)制
發(fā)現(xiàn)其子元素 div 的 innerText 發(fā)生了改變,因此這個(gè) div 的 effectList 變成
[{
instance: div,
type: ‘update’,
property: ‘innerText’
value: 9
}]
Js
復(fù)制
[圖片上傳失敗...(image-6afdd7-1660720729967)]
effect List 如何拼接
會(huì)按照 dfs 的順序拼接 effectList驾诈,子節(jié)點(diǎn)的 effectList 在返回時(shí)拼接到父節(jié)點(diǎn)的頭部缠诅,兄弟節(jié)點(diǎn)會(huì)把第一個(gè)兄弟節(jié)點(diǎn)的 effectList 當(dāng)作頭部按順序進(jìn)行拼接,在本例中乍迄,
- 第二個(gè) div 返回管引,拼到第二個(gè) Item 上,
- div -> Item (1)
- 第三個(gè) div 返回闯两,拼到第三個(gè) Item 上
- div -> Item (2)
- 和 (2) 拼接起來(lái)
- div -> Item -> div -> Item
- 返回 List
- div -> Item -> div -> Item -> List
[圖片上傳失敗...(image-8c31c0-1660720729967)]
事實(shí)上褥伴,就如上一節(jié)所說(shuō),React 會(huì)收集所有被標(biāo)記了 effectTag 元素的 effectList漾狼,但為了抓主要矛盾重慢,在上面的講述中,我重點(diǎn)講述兩個(gè) div 的 effectList
[[
instance: div,
type: ‘update’,
property: ‘innerText’
value: 4
},
{
instance: div,
type: ‘update’,
property: ‘innerText’
value: 9
}]
Js
復(fù)制
React 會(huì)用 effectList 批量的更新真實(shí) dom逊躁,這一階段被稱(chēng)為 commit 階段似踱。它會(huì)一次性的調(diào)用更新真實(shí) dom 的方法,在本例中稽煤,為 dom.innerText = ‘xxx’
之前問(wèn)題的答案
至此為止核芽,我想有的同學(xué)應(yīng)該可以猜出 render 階段和 commit 階段到底誰(shuí)可以被打斷。答案是顯而易見(jiàn)的念脯,render 階段可以被打斷,commit 階段不可以被打斷弯淘。請(qǐng)注意绿店,這里的 render 和 react 內(nèi)部的那個(gè) render 并不是一個(gè)概念。而 commit 才是去更新真實(shí) dom 的階段。如果 commit 被打斷假勿,這就意味著 dom 的更新不是一次性的借嗽,那么,1转培,2恶导,3 可能會(huì)被更新成 1,4浸须,3 而中途被打斷惨寿。這個(gè)對(duì)于使用者而言,顯然是不可以接受的删窒。
尤大對(duì) React Fiber 的評(píng)價(jià)
雖然 React Fiber 這一設(shè)計(jì)被國(guó)外很多人認(rèn)為是高技術(shù)含量的體現(xiàn)裂垦,但是對(duì) Fiber 的評(píng)價(jià)卻褒貶不一,尤雨溪在的 Vue Conf 說(shuō)過(guò):如果我們可以把更新做得足夠快的話(huà)肌索,理論上就不需要時(shí)間分片了蕉拢。他說(shuō):React Fiber 本質(zhì)上是為了解決 React 更新低效率的問(wèn)題,不要期望 Fiber 能給你現(xiàn)有應(yīng)用帶來(lái)質(zhì)的提升, 如果性能問(wèn)題是自己造成的诚亚,自己的鍋還是得自己背.
參考資料
【1】Lin Clark - A Cartoon Intro to Fiber - React Conf 2017 www.youtube.com/watch?v=ZCu…
【2】React Fiber Architecture github.com/acdlite/rea…
【3】Facebook announces React Fiber, a rewrite of its React framework techcrunch.com/2017/04/18/…
【4】這可能是最通俗的 React Fiber(時(shí)間分片) 打開(kāi)方式 juejin.cn/post/684490…
【5】React Fiber 原理介紹 segmentfault.com/a/119000001…