React Fiber剖析

react-fiber-logo.jpeg

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)惶我。

Fiber Tree.png

首先每個節(jié)點是統(tǒng)一的,會有兩個屬性FirstChildNextSibiling博投,第一個指向節(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的做法酥艳,就是剛剛提到的雙緩沖機制

workInProgress-mount

當內(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算法判斷。


workInProgress-update

在更新過程中惰爬,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.png

effectList使得commit階段只需要遍歷effectList就可以了信柿,提高了運行性能, 至此 render階段告一段落醒第。

寫在最后

我覺得React Fiber是一種解決問題的理念架構(gòu)渔嚷,從React16架構(gòu)來說分為三層:
Scheduler/Reconciler/Renderer
它利用瀏覽器的空閑時間完成循環(huán)模擬遞``歸過程,所有操作都在內(nèi)存中進行稠曼,只有所有組件完成Rconciler工作形病,才會走Renderer一次渲染展示,提升效率霞幅。
篇幅不長漠吻,知識點零零總總,性能優(yōu)化沒有最好司恳,只有更好途乃,所以我們一直在路上...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市抵赢,隨后出現(xiàn)的幾起案子欺劳,更是在濱河造成了極大的恐慌,老刑警劉巖铅鲤,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件划提,死亡現(xiàn)場離奇詭異,居然都是意外死亡邢享,警方通過查閱死者的電腦和手機鹏往,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來骇塘,“玉大人伊履,你說我怎么就攤上這事】钗ィ” “怎么了唐瀑?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長插爹。 經(jīng)常有香客問我哄辣,道長,這世上最難降的妖魔是什么赠尾? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任力穗,我火速辦了婚禮,結(jié)果婚禮上气嫁,老公的妹妹穿的比我還像新娘厘灼。我一直安慰自己损趋,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蛾坯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪疏遏。 梳的紋絲不亂的頭發(fā)上脉课,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天财异,我揣著相機與錄音倘零,去河邊找鬼戳寸。 笑死呈驶,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的疫鹊。 我是一名探鬼主播袖瞻,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼拆吆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了枣耀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤牺堰,失蹤者是張志新(化名)和其女友劉穎颅围,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體院促,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了墩邀。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盏浙。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡荔茬,死狀恐怖竹海,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情斋配,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布坏瞄,位于F島的核電站,受9級特大地震影響甩卓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜缀棍,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一机错、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧毡熏,春花似錦、人聲如沸狱窘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽尖奔。三九已至,卻和暖如春淹禾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背铃岔。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工峭火, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留智嚷,地道東北人纺且。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像猜嘱,于是被迫代替她去往敵國和親恐仑。 傳聞我的和親對象是個殘疾皇子泉坐,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

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