React18系列reconciler從0實現(xiàn)過程詳解
引言
本系列是講述從0開始實現(xiàn)一個react18的基本版本。由于React
源碼通過Mono-repo 管理倉庫,我們也是用pnpm
提供的workspaces
來管理我們的代碼倉庫,打包我們使用rollup
進行打包。
我們這一次主要寫有關調(diào)和(reconciler
)和ReactDom,React
將調(diào)和單獨的抽出一個包,暴露出入口,通過不同的宿主環(huán)境去調(diào)用不同的api。
React-Dom包
這個包主要是提供瀏覽器環(huán)境的一些dom操作。主要是提供2個文件hostConfig.ts
以及root.ts
。 想想我們在React18中,是通過如下方式調(diào)用的。所以我們需要提供一個方法createRoot
方法,返回要給包含render函數(shù)的對象。
import ReactDOM from 'react-dom/client'; ReactDOM.createRoot(root).render(<App />)
createRoot
主要功能是2個,第一個是創(chuàng)建根fiberNode
節(jié)點, 第二個創(chuàng)建更新(初始化主要是用于渲染),開始調(diào)度。
//createRoot.ts 文件 import { createContainer, updateContainer, } from "../../react-reconciler/src/filerReconciler"; export function createRoot(container: Container) { const root = createContainer(container); return { render(element: ReactElementType) { updateContainer(element, root); }, }; }
createRoot.js
主要是調(diào)用的react-reconciler
的createContainer
方法和updateContainer
方法。我們之后看看這2個方法主要的作用
hostConfig.ts
主要是創(chuàng)建各種dom,已經(jīng)dom的插入操作
export const createInstance = (type: string, props: any): Instance => { // TODO 處理props const element = document.createElement(type); return element; }; export const appendInitialChild = ( parent: Instance | Container, child: Instance ) => { parent.appendChild(child); }; export const createTextInstance = (content: string) => { return document.createTextNode(content); }; export const appendChildToContainer = appendInitialChild;
React-reconciler包
createContainer() 函數(shù)
從上面我們可以知道,首先調(diào)用的createContainer
和updateContainer
,我們把它寫到filerReconciler.ts
中createContainer
接受傳入的dom元素。
/** * ReactDOM.createRoot()中調(diào)用 * 1. 創(chuàng)建fiberRootNode 和 hostRootFiber。并建立聯(lián)系 * @param {Container} container */ export function createContainer(container: Container) { const hostRootFiber = new FiberNode(HostRoot, {}, null); const fiberRootNode = new FiberRootNode(container, hostRootFiber); hostRootFiber.updateQueue = createUpdateQueue(); return fiberRootNode; }
可以看到我們在這里主要就是2個事情
調(diào)用了2個方法去創(chuàng)建2個不同的fiberNode,一個是hostRootFiber
,一個是fiberRootNode
創(chuàng)建一個更新隊列,并將其賦值給hostRootFiber
/** * 頂部節(jié)點 */ export class FiberRootNode { container: Container; // 不同環(huán)境的不同的節(jié)點 在瀏覽器環(huán)境 就是 root節(jié)點 current: FiberNode; finishedWork: FiberNode | null; // 遞歸完成后的hostRootFiber constructor(container: Container, hostRootFiber: FiberNode) { this.container = container; this.current = hostRootFiber; hostRootFiber.stateNode = this; this.finishedWork = null; } } export class FiberNode { constructor(tag: WorkTag, pendingProps: Props, key: Key) { this.tag = tag; this.pendingProps = pendingProps; this.key = key; this.stateNode = null; // dom引用 this.type = null; // 組件本身 FunctionComponent () => {} // 樹狀結(jié)構(gòu) this.return = null; // 指向父fiberNode this.sibling = null; // 兄弟節(jié)點 this.child = null; // 子節(jié)點 this.index = 0; // 兄弟節(jié)點的索引 this.ref = null; // 工作單元 this.pendingProps = pendingProps; // 等待更新的屬性 this.memoizedProps = null; // 正在工作的屬性 this.memoizedState = null; this.updateQueue = null; this.alternate = null; // 雙緩存樹指向(workInProgress 和 current切換) this.flags = NoFlags; // 副作用標識 this.subtreeFlags = NoFlags; // 子樹中的副作用 } }
接下來,我們看看createUpdateQueue
里面的執(zhí)行邏輯。執(zhí)行了一個函數(shù),返回了一個對象。所以現(xiàn)在hostRootFiber
的updateQueue
指向了這個指針
/** * 初始化updateQueue * @returns {UpdateQueue<Action>} */ export const createUpdateQueue = <State>() => { return { shared: { pending: null, }, } as UpdateQueue<State>; };
我們從上面createRoot
執(zhí)行完后,返回了一個render函數(shù),我們接下來看看render后的執(zhí)行過程,是怎么渲染到頁面的。
render() 調(diào)用
createRoot
執(zhí)行后,創(chuàng)建了一個rootFiberNode
, 并返回了render
調(diào)用,主要是執(zhí)行了updateContainer
用于去渲染初始化的工作。
updateContainer
接受2個參數(shù),第一個參數(shù)是傳入的ReactElement
(), 第二個參數(shù)是fiberRootNode
。
主要是做3件事情:
- 創(chuàng)建一個更新事件
- 把更新事件推進隊列中
- 調(diào)用調(diào)度,開始更新
/** * ReactDOM.createRoot().render 中調(diào)用更新 * 1. 創(chuàng)建update, 并將其推到enqueueUpdate中 */ export function updateContainer( element: ReactElementType | null, root: FiberRootNode ) { const hostRootFiber = root.current; const update = createUpdate<ReactElementType | null>(element); enqueueUpdate( hostRootFiber.updateQueue as UpdateQueue<ReactElementType | null>, update ); // 插入更新后,進入調(diào)度 scheduleUpdateOnFiber(hostRootFiber); return element; }
創(chuàng)建更新createUpdate
實際上就是創(chuàng)建一個對象,由于初始化的時候傳入的是ReactElementType(), 所以返回的是App對應的ReactElement對象
/** * 創(chuàng)建更新 * @param {Action<State>} action * @returns {Update<State>} */ export const createUpdate = (action) => { return { action, }; };
將更新推進隊列enqueueUpdate
接受2個參數(shù),第一個參數(shù)是我們創(chuàng)建一個更新隊列的引用,第二個是新增的隊列
/** * 更新update * @param {UpdateQueue<Action>} updateQueue * @param {Update<Action>} update */ export const enqueueUpdate = <State>( updateQueue: UpdateQueue<State>, update: Update<State> ) => { updateQueue.shared.pending = update; };
執(zhí)行到這一步驟,我們得到了更新隊列,其實是一個ReactElement
組件 及我們調(diào)用render傳入的jsx對象。
開始調(diào)用scheduleUpdateOnFiber
接受FiberNode
開始執(zhí)行我們的渲染工作, 一開始渲染傳入的是hostFiberNode
之后其他更新傳遞的是對應的fiberNode
export function scheduleUpdateOnFiber(fiber: FiberNode) { // todo 調(diào)度功能 let root = markUpdateFromFiberToRoot(fiber); renderRoot(root); }
wookLoop
執(zhí)行完上面的操作后,接下來進入的調(diào)和階段。開始我們要明白一個關鍵詞:
workInProgress
: 表示當前正在調(diào)和的fiber節(jié)點,之后簡稱wip
beginWork
: 主要是根據(jù)當前fiberNode
創(chuàng)建下一級fiberNode,在update時標記placement
(新增、移動)ChildDeletion
(刪除)
completeWork
: 在mount時構(gòu)建Dom Tree, 初始化屬性,在Update時標記Update
(屬性更新),最終執(zhí)行flags冒泡
flags
冒泡我們下一節(jié)講。
從上面我們可以看到調(diào)用了scheduleUpdateOnFiber
方法,開始從根部渲染頁面。scheduleUpdateOnFiber
主要是執(zhí)行了2個方法:
markUpdateFromFiberToRoot
: 由于我們更新的節(jié)點可能不是hostfiberNode
, 這個方法就是不管傳入的是那個節(jié)點,返回我們的根節(jié)點rootFiberNode
// 從當前觸發(fā)更新的fiber向上遍歷到根節(jié)點fiber function markUpdateFromFiberToRoot(fiber: FiberNode) { let node = fiber; let parent = node.return; while (parent !== null) { node = parent; parent = node.return; } if (node.tag === HostRoot) { return node.stateNode; } return null; }
renderRoot: 這里是我們wookLoop的入口,也是調(diào)和完成后,將生成的fiberNode樹,賦值給finishedWork,并掛在根節(jié)點上,進入commit
的入口。
function renderRoot(root: FiberRootNode) { // 初始化,將workInProgress 指向第一個fiberNode prepareFreshStack(root); do { try { workLoop(); break; } catch (e) { if (__DEV__) { console.warn("workLoop發(fā)生錯誤", e); } workInProgress = null; } } while (true); const finishedWork = root.current.alternate; root.finishedWork = finishedWork; // wip fiberNode樹 樹中的flags執(zhí)行對應的操作 commitRoot(root); }
prepareFreshStack
函數(shù): 用于初始化當前節(jié)點的wip, 并創(chuàng)建alternate 的雙緩存的建立。 由于我們開始的時候傳入的hostFiberNode
, 經(jīng)過createWorkInProgress
后,創(chuàng)建了一個新的fiberNode 并通過alternate相互指向。并賦值給wip
let workInProgress: FiberNode | null = null; function prepareFreshStack(root: FiberRootNode) { workInProgress = createWorkInProgress(root.current, {}); } export const createWorkInProgress = ( current: FiberNode, pendingProps: Props ): FiberNode => { let wip = current.alternate; if (wip === null) { //mount wip = new FiberNode(current.tag, pendingProps, current.key); wip.stateNode = current.stateNode; wip.alternate = current; current.alternate = wip; } else { //update wip.pendingProps = pendingProps; // 清掉副作用(上一次更新遺留下來的) wip.flags = NoFlags; wip.subtreeFlags = NoFlags; } wip.type = current.type; wip.updateQueue = current.updateQueue; wip.child = current.child; wip.memoizedProps = current.memoizedProps; wip.memoizedState = current.memoizedState; return wip; };
接下來我們來分析一下workLoop中到底是如何生成fiberNode樹的。它本身函數(shù)執(zhí)行很簡單。就是不停的根據(jù)wip
進行單個fiberNode的處理。 此時wip指向的hostRootFiber。開始執(zhí)行performUnitOfWork
進行遞歸操作,其中遞:beginWork
,歸:completeWork
。React通過DFS,首先找到對應的葉子節(jié)點。
function workLoop() { while (workInProgress !== null) { performUnitOfWork(workInProgress); } } function performUnitOfWork(fiber: FiberNode): void { const next = beginWork(fiber); // next 是fiber的子fiber 或者 是null // 工作完成,需要將pendingProps 復制給 已經(jīng)渲染的props fiber.memoizedProps = fiber.pendingProps; if (next === null) { // 沒有子fiber completeUnitOfWork(fiber); } else { workInProgress = next; } }
beginWork開始
主要是向下進行遍歷,創(chuàng)建不同的fiberNode。由于我們傳入的是HostRoot,所以會走到updateHostRoot
分支
/** * 遞歸中的遞階段 * 比較 然后返回子fiberNode 或者null */ export const beginWork = (wip: FiberNode) => { switch (wip.tag) { case HostRoot: return updateHostRoot(wip); case HostComponent: return updateHostComponent(wip); case HostText: // 文本節(jié)點沒有子節(jié)點,所以沒有流程 return null; default: if (__DEV__) { console.warn("beginWork未實現(xiàn)的類型"); } break; } return null; };
updateHostRoot
這個方法主要是2個部分:
- 根據(jù)我們之前創(chuàng)建的更新隊列獲取到最新的值
- 創(chuàng)建子fiber
/** processUpdateQueue: 是根據(jù)不同的類型(函數(shù)和其他)生成memoizedState */ function updateHostRoot(wip: FiberNode) { const baseState = wip.memoizedState; const updateQueue = wip.updateQueue as UpdateQueue<ElementType>; // 這里獲取之前的更新隊列 const pending = updateQueue.shared.pending; updateQueue.shared.pending = null; const { memoizedState } = processUpdateQueue(baseState, pending); // 最新狀態(tài) wip.memoizedState = memoizedState; // 其實就是傳入的element const nextChildren = wip.memoizedState; // 就是我們傳入的ReactElement 對象 reconcileChildren(wip, nextChildren); return wip.child; }
reconcileChildren
調(diào)和子節(jié)點, 根據(jù)是否生成過,分別調(diào)用不同的方法。通過上面我們知道傳入的hostFiber
, 此時是存在alternate
屬性的,所以會走到reconcilerChildFibers
分支。
根據(jù)當前傳入的returnFiber
是hostFiberNode
以及currentFiber
為null,newChild
為ReactElementType。我們可以判斷接下來會走到reconcileSingleElement
的執(zhí)行。其中placeSingleChild
是打標記使用的,我們暫時先不研究。
/** wip: 當前正在執(zhí)行的父fiberNode children: 即將要生成的子fiberNode */ function reconcileChildren(wip: FiberNode, children?: ReactElementType) { const current = wip.alternate; if (current !== null) { // update wip.child = reconcilerChildFibers(wip, current?.child, children); } else { // mount wip.child = mountChildFibers(wip, null, children); } } function reconcilerChildFibers( returnFiber: FiberNode, currentFiber: FiberNode | null, newChild?: ReactElementType | string | number ) { // 判斷當前fiber的類型 if (typeof newChild === "object" && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: return placeSingleChild( reconcileSingleElement(returnFiber, currentFiber, newChild) ); default: if (__DEV__) { console.warn("未實現(xiàn)的reconcile類型", newChild); } break; } } // Todo 多節(jié)點的情況 ul > li * 3 // HostText if (typeof newChild === "string" || typeof newChild === "number") { return placeSingleChild( reconcileSingleTextNode(returnFiber, currentFiber, newChild) ); } if (__DEV__) { console.warn("未實現(xiàn)的reconcile類型", newChild); } return null; }; }
reconcileSingleElement
從名字我們可以看出是通過ReactElement 創(chuàng)建單一的fiberNode。通過reconcileSingleElement
我們就可以得出了一個新的子節(jié)點,然后通過return指向父fiber。此時的fiberNode樹如下圖。
/** * 根據(jù)reactElement對象創(chuàng)建fiber并返回 */ function reconcileSingleElement( returnFiber: FiberNode, _currentFiber: FiberNode | null, element: ReactElementType ) { const fiber = createFiberFromElement(element); fiber.return = returnFiber; return fiber; } export function createFiberFromElement(element: ReactElementType): FiberNode { const { type, key, props } = element; let fiberTag: WorkTag = FunctionComponent; if (typeof type === "string") { // <div/> type : 'div' fiberTag = HostComponent; } else if (typeof type !== "function" && __DEV__) { console.log("未定義的type類型", element); } const fiber = new FiberNode(fiberTag, props, key); fiber.type = type; return fiber; }
調(diào)用完后,此時回到了reconcileChildren
函數(shù)的這一句代碼執(zhí)行,指定wip的child指向。此時函數(shù)執(zhí)行完畢。
// 省略無關代碼 function reconcileChildren(wip: FiberNode, children?: ReactElementType) { wip.child = reconcilerChildFibers(wip, current?.child, children); }
執(zhí)行完后返回updateHostRoot
函數(shù)調(diào)用reconcileChildren
的地方。然后返回wip的child。
function updateHostRoot(wip) { const baseState = wip.memoizedState; reconcileChildren(wip, nextChildren); return wip.child; }
執(zhí)行完updateHostRoot
函數(shù)后,返回調(diào)用它的beginWork
中。beginWork
也同樣返回了當前wip的child節(jié)點。
export const beginWork = (wip: FiberNode) => { switch (wip.tag) { case HostRoot: return updateHostRoot(wip); } }
執(zhí)行完后,我們最后又回到了最開始調(diào)用beginWork
的地方。進行接下來的操作,主要是將已經(jīng)渲染過的屬性賦值。然后將wip賦值給下一個剛剛生成的子節(jié)點。以便于開始下一次的遞歸中調(diào)用。
function performUnitOfWork(fiber) { const next = beginWork(fiber); // next 是fiber的子fiber 或者 是null // 工作完成,需要將pendingProps 復制給 已經(jīng)渲染的props fiber.memoizedProps = fiber.pendingProps; if (next === null) { // 沒有子fiber completeUnitOfWork(fiber); } else { workInProgress = next; } }
由于workInProgress
不等于null, 說明還有子節(jié)點。繼續(xù)進行workLoop
調(diào)用。又開始了新的一輪。直到我們到達了葉子節(jié)點。
function workLoop() { while (workInProgress !== null) { performUnitOfWork(workInProgress); } }
例子
例如,如下例子,當遍歷到hcc文本節(jié)點后,由于我們節(jié)點是沒有調(diào)和流程的。所以執(zhí)行到beginWork
后,返回了一個null。正式結(jié)束了遞歸調(diào)用中的“遞" 過程。此時的fiberNode樹如下圖所示。
const jsx = <div><span>hcc</span></div> const root = document.querySelector('#root') ReactDOM.createRoot(root).render(jsx)
completeWork開始
從上面的beginWork
操作后,此時我們wip在文本節(jié)點hcc
的節(jié)點位置.
completeUnitOfWork
接下來執(zhí)行performUnitOfWork
中的completeUnitOfWork
的邏輯部分,我們看看completeUnitOfWork
的邏輯部分。 我們傳入的最底部的葉子節(jié)點。首先會對當前節(jié)點進行completeWork
的方法調(diào)用。
function completeUnitOfWork(fiber) { let node = fiber; do { completeWork(node); const sibling = node.sibling; if (sibling !== null) { workInProgress = sibling; return; } node = node.return; workInProgress = node; } while (node !== null); }
completeWork
首次我們會接受到一個最底部的子fiberNode,由于是第一次mount,所以當前的fiber下不會存在alternate
屬性的,所以會走到構(gòu)建Dom的流程。
/** * 遞歸中的歸 */ export const completeWork = (wip: FiberNode) => { const newProps = wip.pendingProps; const current = wip.alternate; switch (wip.tag) { case HostComponent: if (current !== null && wip.stateNode) { //update } else { // 1. 構(gòu)建DOM const instance = createInstance(wip.type, newProps); // 2. 將DOM插入到DOM樹中 appendAllChildren(instance, wip); wip.stateNode = instance; } bubbleProperties(wip); return null; case HostText: if (current !== null && wip.stateNode) { //update } else { // 1. 構(gòu)建DOM const instance = createTextInstance(newProps.content); // 2. 將DOM插入到DOM樹中 wip.stateNode = instance; } bubbleProperties(wip); return null; case HostRoot: bubbleProperties(wip); return null; default: if (__DEV__) { console.warn("未實現(xiàn)的completeWork"); } break; } }; // 根據(jù)邏輯判斷,走到下面的邏輯判斷,傳入了文本 // 1. 構(gòu)建DOM const instance = createTextInstance(newProps.content); // 2. 將DOM插入到DOM樹中 wip.stateNode = instance;
經(jīng)過completeWork
后,我們給當前的wip添加了stateNode
屬性,用于指向生成的Dom節(jié)點。 執(zhí)行完completeWork
后,繼續(xù)返回到completeUnitOfWork
中,查找sibling
節(jié)點,目前我們demo中沒有,所以會向上找到當前節(jié)點的return指向。繼續(xù)執(zhí)行completeWork
工作,此時的結(jié)構(gòu)變成了如下圖:
由于我們wip目前是HostComponent
, 所以走到了如下的completeWork
的邏輯。這里 根據(jù)type
創(chuàng)建不同的Dom元素,和之前一樣,綁定到對應的stateNode
屬性上。我們可以看到除了這2個,還執(zhí)行了一個函數(shù)appendAllChildren
。我們?nèi)タ纯催@個函數(shù)的作用是什么
// 1. 構(gòu)建DOM const instance = createInstance(wip.type); // 2. 將DOM插入到DOM樹中 appendAllChildren(instance, wip); wip.stateNode = instance;
appendAllChildren
接受2個參數(shù),第一個是剛剛通過wip
的type生成的對應的dom, 另外一個是wip
本身。 它的作用就是把我們上一步產(chǎn)生的dom節(jié)點,插入到剛剛產(chǎn)生的父dom節(jié)點上,形成一個局部的小dom樹。
它本身存在一個復雜的遍歷過程,因為fiberNode
的層級和DOM元素的層級可能不是一一對應的。
/** * 在parent的節(jié)點下,插入wip * @param {FiberNode} parent * @param {FiberNode} wip */ function appendAllChildren(parent: Container, wip: FiberNode) { let node = wip.child; while (node !== null) { if (node?.tag === HostComponent || node?.tag === HostText) { appendInitialChild(parent, node?.stateNode); } else if (node.child !== null) { node.child.return = node; // 繼續(xù)向下查找 node = node.child; continue; } if (node === wip) { return; } while (node.sibling === null) { if (node.return === null || node.return === wip) { return; } // 向上找 node = node?.return; } node.sibling.return = node.return; node = node.sibling; } }
我們用這個圖來說明一下流程
- 當前的”歸“到了
div
對應的fiberNode。我們獲取到node是第一個子元素的span, 執(zhí)行appendInitialChild
方法,把對應的stateNode
的dom節(jié)點插入parent中。 - 接下來執(zhí)行由于
node.sibling
不為空,所以會將node 復制給第二個span。然后繼續(xù)執(zhí)行appendInitialChild
。以此執(zhí)行到第三個span節(jié)點。 - 第三個span節(jié)點對應的
sibling
為空,所以開始向上查找到node.return === wip
結(jié)束函數(shù)調(diào)用。 - 此時三個span產(chǎn)生的dom,都已經(jīng)插入到
parent(div dom)
中。
回到completeUnitOfWork
經(jīng)過上述操作后,我們繼續(xù)回到completeUnitOfWork
的調(diào)用,繼續(xù)向上歸并。到上述例子的div
節(jié)點。直到我們遍歷到hostFiberNode
, 它是沒有return
屬性的,所以返回null,結(jié)束了completeUnitOfWork
的執(zhí)行?;氐搅俗铋_始的workLoop
。此時的workInProgress
等于null, 結(jié)束循環(huán)。
function workLoop() { while (workInProgress !== null) { performUnitOfWork(workInProgress); } }
回到renderRoot
執(zhí)行完workLoop
, 就回到了renderRoot
的部分。此時我們已經(jīng)得到了完整的fiberNode樹,以及相應的dom元素。此時對應的結(jié)果如下圖:
那么生成的fiberNode樹是如何渲染的界面上的,我們下一章的commit章節(jié)介紹,如何打標簽和渲染,更多關于React18系列reconciler實現(xiàn)的資料請關注腳本之家其它相關文章!
相關文章
React中的setState使用細節(jié)和原理解析(最新推薦)
這篇文章主要介紹了React中的setState使用細節(jié)和原理解析(最新推薦),前面我們有使用過setState的基本使用, 接下來我們對setState使用進行詳細的介紹,需要的朋友可以參考下2022-12-12react-router browserHistory刷新頁面404問題解決方法
本篇文章主要介紹了react-router browserHistory刷新頁面404問題解決方法,非常具有實用價值,需要的朋友可以參考下2017-12-12react hooks實現(xiàn)防抖節(jié)流的方法小結(jié)
這篇文章主要介紹了react hooks實現(xiàn)防抖節(jié)流的幾種方法,文中通過代碼示例給大家講解的非常詳細,對大家的學習或工作有一定的幫助,需要的朋友可以參考下2024-04-04