React Fiber與調(diào)和深入分析
一 引沿
Fiber 架構(gòu)是React16中引入的新概念,目的就是解決大型 React 應(yīng)用卡頓,React在遍歷更新每一個(gè)節(jié)點(diǎn)的時(shí)候都不是用的真實(shí)DOM,都是采用虛擬DOM,所以可以理解成fiber就是React的虛擬DOM,更新Fiber的過程叫做調(diào)和,每一個(gè)fiber都可以作為一個(gè)執(zhí)行單元來處理,所以每一個(gè) fiber 可以根據(jù)自身的過期時(shí)間expirationTime,來判斷是否還有空間時(shí)間執(zhí)行更新,如果沒有時(shí)間更新,就要把主動(dòng)權(quán)交給瀏覽器去渲染,做一些動(dòng)畫,重排( reflow ),重繪 repaints 之類的事情,這樣就能給用戶感覺不是很卡。
二 什么是調(diào)和
調(diào)和是一種算法,就是React對(duì)比新老虛擬DOM的過程,以決定需要更新哪一部分。
三 什么是Filber
Fiber的目的是為了讓React充分利用調(diào)度,以便做到如下幾點(diǎn):
- 暫停工作,稍后再回來
- 優(yōu)先考慮不同類型的工作
- 重用以前完成的工作
- 如果不再需要,則中止工作
為了實(shí)現(xiàn)上面的要求,我們需要把任務(wù)拆分成一個(gè)個(gè)可執(zhí)行的單元,這些可執(zhí)行的單元就叫做一個(gè)Fiber,一個(gè)Fiber就代表一個(gè)可執(zhí)行的單元。
一個(gè)Fiber就是一個(gè)普通的JS對(duì)象,包含一些組件的相關(guān)信息。
function FiberNode(){ this.tag = tag; // fiber 標(biāo)簽 證明是什么類型fiber。 this.key = key; // key調(diào)和子節(jié)點(diǎn)時(shí)候用到。 this.type = null; // dom元素是對(duì)應(yīng)的元素類型,比如div,組件指向組件對(duì)應(yīng)的類或者函數(shù)。 this.stateNode = null; // 指向?qū)?yīng)的真實(shí)dom元素,類組件指向組件實(shí)例,可以被ref獲取。 this.return = null; // 指向父級(jí)fiber this.child = null; // 指向子級(jí)fiber this.sibling = null; // 指向兄弟fiber this.index = 0; // 索引 this.ref = null; // ref指向,ref函數(shù),或者ref對(duì)象。 this.pendingProps = pendingProps;// 在一次更新中,代表element創(chuàng)建 this.memoizedProps = null; // 記錄上一次更新完畢后的props this.updateQueue = null; // 類組件存放setState更新隊(duì)列,函數(shù)組件存放 this.memoizedState = null; // 類組件保存state信息,函數(shù)組件保存hooks信息,dom元素為null this.dependencies = null; // context或是時(shí)間的依賴項(xiàng) this.mode = mode; //描述fiber樹的模式,比如 ConcurrentMode 模式 this.effectTag = NoEffect; // effect標(biāo)簽,用于收集effectList this.nextEffect = null; // 指向下一個(gè)effect this.firstEffect = null; // 第一個(gè)effect this.lastEffect = null; // 最后一個(gè)effect this.expirationTime = NoWork; // 通過不同過期時(shí)間,判斷任務(wù)是否過期, 在v17版本用lane表示。 this.alternate = null; //雙緩存樹,指向緩存的fiber。更新階段,兩顆樹互相交替。 }
type 就是react的元素類型
export const FunctionComponent = 0; // 對(duì)應(yīng)函數(shù)組件 export const ClassComponent = 1; // 對(duì)應(yīng)的類組件 export const IndeterminateComponent = 2; // 初始化的時(shí)候不知道是函數(shù)組件還是類組件 export const HostRoot = 3; // Root Fiber 可以理解為跟元素 , 通過reactDom.render()產(chǎn)生的根元素 export const HostPortal = 4; // 對(duì)應(yīng) ReactDOM.createPortal 產(chǎn)生的 Portal export const HostComponent = 5; // dom 元素 比如 <div> export const HostText = 6; // 文本節(jié)點(diǎn) export const Fragment = 7; // 對(duì)應(yīng) <React.Fragment> export const Mode = 8; // 對(duì)應(yīng) <React.StrictMode> export const ContextConsumer = 9; // 對(duì)應(yīng) <Context.Consumer> export const ContextProvider = 10; // 對(duì)應(yīng) <Context.Provider> export const ForwardRef = 11; // 對(duì)應(yīng) React.ForwardRef export const Profiler = 12; // 對(duì)應(yīng) <Profiler/ > export const SuspenseComponent = 13; // 對(duì)應(yīng) <Suspense> export const MemoComponent = 14; // 對(duì)應(yīng) React.memo 返回的組件
比如元素結(jié)構(gòu)如下:
export default class Parent extends React.Component{ render(){ return <div> <h1>hello,world</h1> <Child /> </div> } } function Child() { return <p>child</p> }
對(duì)應(yīng)的Filber結(jié)構(gòu)如下:
有了上面的概念后我們就自己實(shí)現(xiàn)一個(gè)Fiber的更新機(jī)制
四 實(shí)現(xiàn)調(diào)和的過程
我們通過渲染一段jsx來說明React的調(diào)和過程,也就是我們要手寫實(shí)現(xiàn)ReactDOM.render()
const jsx = ( <div className="border"> <h1>hello</h1> <a rel="external nofollow" >React</a> </div> ) ReactDOM.render( jsx, document.getElementById('root') );
1. 創(chuàng)建FiberRoot
react-dom.js
function createFiberRoot(element, container){ return { type: container.nodeName.toLocaleLowerCase(), props: { children: element }, stateNode: container } } function render(element, container) { const FibreRoot = createFiberRoot(element, container) scheduleUpdateOnFiber(FibreRoot) } export default { render }
參考React實(shí)戰(zhàn)視頻講解:進(jìn)入學(xué)習(xí)
2. render階段
調(diào)和的核心是render和commit,本文不講調(diào)度過程,我們會(huì)簡(jiǎn)單的用requestIdleCallback代替React的調(diào)度過程。
ReactFiberWorkloop.js
let wipRoot = null // work in progress let nextUnitOfwork = null // 下一個(gè)fiber節(jié)點(diǎn) export function scheduleUpdateOnFiber(fiber) { wipRoot = fiber nextUnitOfwork = fiber } function workLoop(IdleDeadline) { while(nextUnitOfwork && IdleDeadline.timeRemaining() > 0) { nextUnitOfwork = performUnitOfWork(nextUnitOfwork) } } function performUnitOfWork() {} requestIdleCallback(workLoop)
每一個(gè) fiber 可以看作一個(gè)執(zhí)行的單元,在調(diào)和過程中,每一個(gè)發(fā)生更新的 fiber 都會(huì)作為一次 workInProgress 。那么 workLoop 就是執(zhí)行每一個(gè)單元的調(diào)度器,如果渲染沒有被中斷,那么 workLoop 會(huì)遍歷一遍 fiber 樹
performUnitOfWork 包括兩個(gè)階段:
- 是向下調(diào)和的過程,就是由 fiberRoot 按照 child 指針逐層向下調(diào)和,期間會(huì)執(zhí)行函數(shù)組件,實(shí)例類組件,diff 調(diào)和子節(jié)點(diǎn)
- 是向上歸并的過程,如果有兄弟節(jié)點(diǎn),會(huì)返回 sibling兄弟,沒有返回 return 父級(jí),一直返回到 fiebrRoot
這么一上一下,構(gòu)成了整個(gè) fiber 樹的調(diào)和。
import { updateHostComponent } from './ReactFiberReconciler' function performUnitOfWork(wip) { // 1. 更新wip const { type } = wip if (isStr(type)) { // type是string,更新普通元素節(jié)點(diǎn) updateHostComponent(wip) } else if (isFn(type)) { // ... } // 2. 返回下一個(gè)要更新的任務(wù) 深度優(yōu)先遍歷 if (wip.child) { return wip.child } let next = wip while(next) { if (next.sibling) { return next.sibling } next = next.return } return null }
根據(jù)type類型區(qū)分是FunctionComponent/ClassComponent/HostComponent/…
本文中只處理HostComponent類型,其他類型的處理可以看文末的完整代碼鏈接。
ReactFiberReconciler.js
import { createFiber } from './createFiber' export function updateHostComponent(wip) { if (!wip.stateNode) { wip.stateNode = document.createElement(wip.type); updateNode(wip.stateNode, wip.props); } // 調(diào)和子節(jié)點(diǎn) reconcileChildren(wip, wip.props.children); } function reconcileChildren(returnFiber, children) { if (isStr(children)) { return } const newChildren = isArray(children) ? children : [children]; let previousNewFiber = null for(let i = 0; i < newChildren.length; i++) { const newChild = newChildren[i]; const newFiber = createFiber(newChild, returnFiber) if (previousNewFiber === null) { returnFiber.child = newFiber } else { previousNewFiber.sibling = newFiber } previousNewFiber = newFiber } } function updateNode(node, nextVal) { Object.keys(nextVal).forEach((k) => { if (k === "children") { if (isStringOrNumber(nextVal[k])) { node.textContent = nextVal[k]; } } else { node[k] = nextVal[k]; } }); }
createFiber.js
export function createFiber(vnode, returnFiber) { const newFiber = { type: vnode.type, // 標(biāo)記節(jié)點(diǎn)類型 key: vnode.key, // 標(biāo)記節(jié)點(diǎn)在當(dāng)前層級(jí)下的唯一性 props: vnode.props, // 屬性 stateNode: null, // 如果組件是原生標(biāo)簽則是dom節(jié)點(diǎn),如果是類組件則是類實(shí)例 child: null, // 第一個(gè)子節(jié)點(diǎn) return: returnFiber,// 父節(jié)點(diǎn) sibling: null, // 下一個(gè)兄弟節(jié)點(diǎn) }; return newFiber; }
至此已經(jīng)完成了render階段,下面是commit階段,commit階段就是依據(jù)Fiber結(jié)構(gòu)操作DOM
function workLoop(IdleDeadline) { while(nextUnitOfwork && IdleDeadline.timeRemaining() > 0) { nextUnitOfwork = performUnitOfWork(nextUnitOfwork) } // commit if (!nextUnitOfwork && wipRoot) { commitRoot(); } } function commitRoot() { commitWorker(wipRoot.child) wipRoot = null; } function commitWorker(wip) { if (!wip) { return } // 1. 提交自己 const { stateNode } = wip let parentNode = wip.return.stateNode if (stateNode) { parentNode.appendChild(stateNode); } // 2. 提交子節(jié)點(diǎn) commitWorker(wip.child); // 3. 提交兄弟節(jié)點(diǎn) commitWorker(wip.sibling); }
五 總結(jié)
- Fiber結(jié)構(gòu),F(xiàn)iber的生成過程。
- 調(diào)和過程,以及 render 和 commit 兩大階段。
到此這篇關(guān)于React Fiber與調(diào)和深入分析的文章就介紹到這了,更多相關(guān)React Fiber與調(diào)和內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
在?React?中使用?Context?API?實(shí)現(xiàn)跨組件通信的方法
在React中,ContextAPI是一個(gè)很有用的特性,可用于組件間的狀態(tài)共享,它允許跨組件傳遞數(shù)據(jù)而無需通過每個(gè)組件手動(dòng)傳遞props,本文給大家介紹在?React?中如何使用?Context?API?來實(shí)現(xiàn)跨組件的通信,感興趣的朋友一起看看吧2024-09-09React中useState原理的代碼簡(jiǎn)單實(shí)現(xiàn)
要實(shí)現(xiàn)useState的背后原理,則需要深入了解狀態(tài)是如何在函數(shù)組件的渲染周期中保持和更新的,本文將通過一段代碼簡(jiǎn)單闡述useState鉤子函數(shù)的實(shí)現(xiàn)思路,希望對(duì)大家有所幫助2023-12-12React Router 5.1.0使用useHistory做頁(yè)面跳轉(zhuǎn)導(dǎo)航的實(shí)現(xiàn)
本文主要介紹了React Router 5.1.0使用useHistory做頁(yè)面跳轉(zhuǎn)導(dǎo)航的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11Create?react?app修改webapck配置導(dǎo)入文件alias
這篇文章主要為大家介紹了Create?react?app修改webapck配置導(dǎo)入文件alias,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12React Native 中實(shí)現(xiàn)確認(rèn)碼組件示例詳解
這篇文章主要為大家介紹了React Native中實(shí)現(xiàn)確認(rèn)碼組件示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08React學(xué)習(xí)之事件綁定的幾種方法對(duì)比
這篇文章主要給大家介紹了關(guān)于React學(xué)習(xí)之事件綁定的幾種方法對(duì)比,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-09-09react-native-video實(shí)現(xiàn)視頻全屏播放的方法
這篇文章主要介紹了react-native-video實(shí)現(xiàn)視頻全屏播放的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-03-03