React自從2013年5月開源以來,一路披襟斬棘到前端最熱門框架之一住册,框架本身具有以下特性耘成。
- Declarative(聲明式)
- Component-Based(組件式)
- Learn Once, Write Anywhere(多端渲染式)
除此之外還有快速高效等特點谓传,主要得益于Virtual Dom的應(yīng)用,虛擬Dom是一種HTML DOM節(jié)點的抽象描述聘殖,存在JS中的結(jié)構(gòu)對象中晨雳,當渲染時通過Diff算法,找到需要變更的節(jié)點進行更新奸腺,這樣就節(jié)省了不必要的更新餐禁。
React快速響應(yīng)主要制約于CPU瓶頸
,比如以下栗子所示:
function App() {
const len = 3000;
return (
<ul>
{Array(len).fill(0).map((_, i) => <li>{i}</li>)}
</ul>
}
const rootEl = document.querySelector("#root");
ReactDOM.render(<App/>, rootEl);
當需要被渲染的節(jié)點很多時洋机,有存在大量的JS計算坠宴,因為GUI渲染線程
和JS執(zhí)行線程
是互斥的,所以在JS計算的時候就會停止瀏覽器界面渲染行為绷旗,導致頁面感覺卡頓喜鼓。
主流瀏覽器刷新頻率為60Hz,即每(1000ms / 60Hz)16.6ms瀏覽器刷新一次衔肢,也就說渲染一幀的時間必須控制在16ms內(nèi)才能保證不掉幀庄岖。
這段時間內(nèi)需要完成以下操作:
- 腳本執(zhí)行(JavaScript)
- 樣式計算(CSS Object Model)
- 布局(Layout)
- 重繪(Paint)
- 合成(Composite)
即JS->Style->Layout->Paint->Composite
過程
既然JS執(zhí)行比較耗時,能不能中斷或暫停JS的執(zhí)行角骤,把執(zhí)行權(quán)交回給渲染線程呢隅忿?
首先看一下React是怎么去做這事的
// react/packages/scheduler/src/forks/SchedulerHostConfig.default.js
// Scheduler periodically yields in case there is other work on the main
// thread, like user events. By default, it yields multiple times per frame.
// It does not attempt to align with frame boundaries, since most tasks don't
// need to be frame aligned; for those that do, use requestAnimationFrame.
let yieldInterval = 5;
let deadline = 0;
從源碼中可以看到心剥,React每次會利用這部分時間(5ms)更新組件,當超過這個時間React就會將執(zhí)行權(quán)就還給瀏覽器由瀏覽器自主分配執(zhí)行權(quán)背桐,React本身則等待下一幀時間來繼續(xù)被中斷的工作优烧,這就引入了一個時間切片
的概念。將耗時的長任務(wù)拆分到每一幀中链峭,一次執(zhí)行小塊任務(wù)畦娄。總結(jié)來說就是將 同步的更新變成可中斷的異步更新
React v15 Stack Reconciler
ReactDOM.render(<App />, rootEl);
React DOM將<App />傳遞給Reconciler弊仪,此時Reconciler將會檢查App是 函數(shù)
or 類
熙卡?
- 【函數(shù)】 -> App(props)
- 【類】 -> new App(props) 來實例化 App, 并調(diào)用生命周期方法 componentWillMount(),之后調(diào)用 render() 方法來獲取渲染的元素
tips: 面試過程中通常會問函數(shù)組件和類組件励饵,兩者是否都被實例化驳癌?答案就在上面
此過程是基于樹的深度遍歷的遞歸過程(遇到自定義組件就會一直的遞歸下去,直到最原始的HTML標簽)役听,Stack Reconciler 的遞歸一旦進入調(diào)用棧就無法中斷或暫停颓鲜,如果當組件嵌套很深或數(shù)量極多,在16ms內(nèi)無法完成就勢必造成瀏覽器丟幀導致卡頓典予。
剛在上面也提過解決方案就是將 同步的更新變成可中斷的異步更新灾杰,但15版本架構(gòu)不支持異步更新,所以React團隊決定擼起袖子重寫熙参,折騰了兩年多終于在2017/3發(fā)布了可用版本。
React Fiber
在首次渲染中構(gòu)建出虛擬dom樹麦备,后續(xù)更新時(setState)通過diff虛擬dom樹得到dom change孽椰,最后將dom change應(yīng)用到真實dom樹中,Stack Reconciler自頂向下遞歸(mount/update)無法中斷導致主線程上的布局/動畫/交互響應(yīng)無法及時得到處理凛篙,引起卡頓黍匾。
這些問題Fiber Reconciler 能夠解決。
Fiber原意纖維呛梆,工作最小單元锐涯,每次通過ReactDOM.render首次構(gòu)建時都會生成一個FiberNode,接下來具體看下FiberNode結(jié)構(gòu)填物。
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag; // FiberNode類型纹腌,目前總有25種類型,常用的就是FunctionComponent 和 ClassComponent
this.key = key; //和組件Element中的key一致
this.elementType = null;
this.type = null; //Function|String|Symbol|Number|Object
this.stateNode = null; //FiberRoot|DomElement|ReactComponentInstance等綁定的其他對象
// Fiber
this.return = null; // FiberNode|null 父級FiberNode
this.child = null; // FiberNode|null 第一個子FiberNode
this.sibling = null;// FiberNode|null 相鄰的下一個兄弟節(jié)點
this.index = 0; //當前父fiber中的位置
this.ref = null; //和組件Element中的ref一致
this.pendingProps = pendingProps; // Object 新的props
this.memoizedProps = null; // Object 處理后的新props
this.updateQueue = null; // UpdateQueue 即將要變更的狀態(tài)
this.memoizedState = null; //Object 處理后的新state
this.dependencies = null;
this.mode = mode; // number
// 普通模式滞磺,同步渲染升薯,React15-16的生產(chǎn)環(huán)境使用
// 并發(fā)模式,異步渲染击困,React17的生產(chǎn)環(huán)境使用
// 嚴格模式涎劈,用來檢測是否存在廢棄API,React16-17開發(fā)環(huán)境使用
// 性能測試模式,用來檢測哪里存在性能問題蛛枚,React16-17開發(fā)環(huán)境使用
// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null; // render階段的diff過程檢測到fiber的子節(jié)點如果有需要被刪除的節(jié)點
this.lanes = NoLanes; //如果fiber.lanes不為空谅海,則說明該fiber節(jié)點有更新
this.childLanes = NoLanes; //判斷當前子樹是否有更新的重要依據(jù),若有更新蹦浦,則繼續(xù)向下構(gòu)建扭吁,否則直接復用已有的fiber樹
this.alternate = null; //FiberNode|null 候補節(jié)點,緩存之前的Fiber節(jié)點白筹,與雙緩存機制相關(guān)智末,后續(xù)講解
}
所有fiber對象都是FiberNode實例,通過tag來標識類型徒河。通過createFiber初始化FiberNode節(jié)點系馆,代碼如下
const createFiber = function(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
): Fiber {
// $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
return new FiberNode(tag, pendingProps, key, mode);
};
Fiber解決這個問題的解法是把渲染/更新過程拆分成一系列小任務(wù),每次執(zhí)行一小塊顽照,再看是否有剩余時間繼續(xù)下一個任務(wù)由蘑,有則繼續(xù),無則掛起代兵,將執(zhí)行線程歸還尼酿。
Fiber Tree
通過虛擬dom樹,react會再創(chuàng)建一個Fiber Tree植影,不同的Element類型對應(yīng)不同類型的Fiber Node裳擎,在后續(xù)的更新過程中每次重新渲染都會重新創(chuàng)建Element,但是Fiber不會重新創(chuàng)建思币,只會更新自身屬性鹿响。
顧名思義,通過多個Fiber Node組成了一個Fiber Tree谷饿,也是為了滿足Fiber增量更新的特性才拓展出了Fiber Tree結(jié)構(gòu)惶我。
首先每個節(jié)點是統(tǒng)一的,會有兩個屬性
FirstChild
及NextSibiling
博投,第一個指向節(jié)點第一個兒子節(jié)點绸贡,第二個指向下一個兄弟節(jié)點,F(xiàn)iber這種單鏈表結(jié)構(gòu)就可以把整個樹串聯(lián)起來毅哗。同時Fiber Tree在Instance層又新增了額外三個實例:
- effect:每個workInProgress tree節(jié)點上都有一個effect list 存放diff結(jié)果听怕,更新完畢后updateQueue進行收集
- workInProgress: reconcile過程中的快照,工作過程節(jié)點虑绵,用戶不可見
- fiber:用來描述增量更新所需的上下文信息
這里我們著重來理解一下 workInProgress
到底起了什么作用叉跛?首先通過代碼來看下它是如何被創(chuàng)建的
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
// 以下兩句很關(guān)鍵
workInProgress.alternate = current;
current.alternate = workInProgress;
// do something else ...
} else {
// do something else ...
}
// do something else ...
return workInProgress;
}
首先workInProgress一個Fiber節(jié)點,當前節(jié)點的alternate為空時蒸殿,通過createFiber創(chuàng)建筷厘,每次狀態(tài)更新都會產(chǎn)生新的workInProgress Fiber樹鸣峭,通過current與workInProgress的替換完成dom更新, 簡單來說當workInProgress Tree內(nèi)存中構(gòu)建完成后直接替換Fiber Tree的做法酥艳,就是剛剛提到的雙緩沖機制
當內(nèi)存中的workInProgress樹直接構(gòu)建完成后摊溶,直接替換了頁面需要渲染的Fiber樹,這是mount的過程充石。
當頁面其中一個node節(jié)點發(fā)生變更時莫换,會開啟一次新的render階段并構(gòu)建一顆心的workInProgress樹,
這里有個優(yōu)化點就是 因為每個node節(jié)點都有一個alternate屬性互相指向骤铃,在構(gòu)建時會嘗試復用當前current Fiber樹已有的節(jié)點內(nèi)屬性拉岁,是否復用取決于diff算法判斷。
在更新過程中惰爬,React在filbert tree中實際發(fā)生改變的fiber上創(chuàng)建effect喊暖,所有effect構(gòu)成effect list鏈表,在commit階段執(zhí)行撕瞧,實現(xiàn)了只對實際發(fā)生改變的fiber做dom更新陵叽,避免了遍歷整個fiber tree造成性能浪費。每當一個Fiber節(jié)點的flags字段不為NoFlags時丛版,就會把此Fiber節(jié)點添加到effect list中巩掺,根據(jù)每一個effect的effectTag類型執(zhí)行對應(yīng)的dom樹更改。
遞歸Fiber節(jié)點
Fiber架構(gòu)下的每個節(jié)點都會經(jīng)歷遞
及 歸
兩個過程页畦,即beginWork/completeWork胖替。
1、beginWork
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// do something else
}
- current: 當前組件上一次更新的Fiber節(jié)點豫缨,workInProgress.alternate
- workInProgress: 當前組件內(nèi)存的Fiber節(jié)點
- renderlanes: 相關(guān)優(yōu)先級
由于雙緩存機制的存在刊殉,我們可以通過current === null 來判斷組件是處于mount還是uplate,當mount時會根據(jù)fiber.tag創(chuàng)建不同類型的子Fiber節(jié)點州胳,當update時 didReceiveUpdate === false就可以直接復用前一次更新的子Fiber節(jié)點,具體判斷如下:
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false;
switch (workInProgress.tag) {
// do something else
}
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else {
didReceiveUpdate = false;
}
} else {
didReceiveUpdate = false;
}
2逸月、completeWork
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
return null;
case ClassComponent: {
// do something else
return null;
}
case HostRoot: {
// do something else
updateHostContainer(workInProgress);
return null;
}
case HostComponent: {
// do something else
return null;
}
// do something else
}
傳入?yún)?shù)和beginWork
一致栓撞,不做過多講解,completeWork會根據(jù)tag不同調(diào)用不同的處理邏輯碗硬。對于處理的當前節(jié)點是mount還是update階段同樣可以使用current === null 來做判斷瓤湘。由于completeWork屬于“歸”階段的函數(shù),每次調(diào)用appendAllChildren都會將已生成的子孫節(jié)點插入當前生成的dom節(jié)點恩尾,這樣就一個完整的dom樹了弛说。
3、effectList
每個執(zhí)行完completeWork并且存在effectTag的Fiber節(jié)點都會保存在effectList單向鏈表中翰意,同時effectList第一個和最末個Fiber節(jié)點會分別保存在fiber.firstEffect /fiber.lastEffect屬性中木人。
effectList使得commit階段只需要遍歷effectList就可以了信柿,提高了運行性能, 至此 render階段告一段落醒第。
寫在最后
我覺得React Fiber是一種解決問題的理念架構(gòu)渔嚷,從React16架構(gòu)來說分為三層:
Scheduler/Reconciler/Renderer
它利用瀏覽器的空閑時間完成循環(huán)模擬遞``歸
過程,所有操作都在內(nèi)存中進行稠曼,只有所有組件完成Rconciler工作形病,才會走Renderer一次渲染展示,提升效率霞幅。
篇幅不長漠吻,知識點零零總總,性能優(yōu)化沒有最好司恳,只有更好途乃,所以我們一直在路上...