源碼中定義了不同類型節點的枚舉值
組件類型
- 文本節點
- HTML標簽節點
- 函數組件
- 類組件
- 等等
src/react/packages/react-reconciler/src/ReactWorkTags.js
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
什么是fiber
A Fiber is work on a Component that needs to be done or was done. There can be more than one per component.
fiber是指組件上將要完成或者已經完成的任務,每個組件可以一個或者多個。
// 比如一個函數組件FunctionComponent 里面是
<div className="border"><p>段落</p><button>按鈕</button>
</div>
// 那最后的fiber結構
const fiber_ = {type: "div",props: {className: "border",},child: {// 第一個子節點type: "p",props: { children: "段落" },sibling: {// 下一個兄弟節點type: "button",props: { children: "按鈕" },},},
};
fiber結構
為什么需要fiber
-
為什么需要fiber
對于大型項目,組件樹會很大,這個時候遞歸遍歷的成本就會很高,會造成主線程被持續占用,結果就是主線程上的布局、動畫等周期性任務就無法立即得到處理,造成視覺上的卡頓,影響用戶體驗。
-
任務分解的意義
解決上面的問題
-
增量渲染(把渲染任務拆分成塊,勻到多幀)
-
更新時能夠暫停,終止,復用渲染任務
-
給不同類型的更新賦予優先級
-
并發方面新的基礎能力
-
更流暢
創建fiber結構
fiber就是一個js對象來抽象vnode
function createFiber(vnode, returnFiber) {const fiber = {type: vnode.type,key: vnode.key,stateNode: null, // 原生標簽時候指dom節點,類組件時候指的是實例props: vnode.props,child: null, // 第一個子fibersibling: null, // 下一個兄弟fiberreturn: returnFiber, // 父節點// 標記節點是什么類型的flags: Placement,deletions: null, // 要刪除子節點 null或者[]index: null, //當前層級下的下標,從0開始// 記錄上一次的狀態 函數組件和類組件不一樣memorizedState: null,// old fiberalternate: null,};const { type } = vnode;if (isStr(type)) {// 原生標簽fiber.tag = HostComponent;} else if (isFn(type)) {// 函數組件或者是類組件fiber.tag = type.prototype.isComponent ? ClassComponent : FunctionComponent;} else if (isUndefined(type)) {fiber.tag = HostText;fiber.props = { children: vnode };} else {fiber.tag = Fragment;}return fiber;
}
深度優先遍歷每個fiber
對不同的類型節點tag,都有對應的處理方法
function performUnitOfWork() {const { tag } = wip;switch (tag) {// 原生標簽 比如div span button p acase HostComponent:updateHostComponent(wip);break;case FunctionComponent:updateFunctionComponent(wip);break;case ClassComponent:updateClassComponent(wip);break;case Fragment:updateFragmentComponent(wip);break;case HostText:updateHostTextComponent(wip);break;default:break;}if (wip.child) {wip = wip.child;return;}let next = wip;while (next) {if (next.sibling) {wip = next.sibling;return;}next = next.return;}wip = null;
}
初次渲染
在react項目中我們都是通過以下方法來初始化組件
ReactDOM.createRoot(document.getElementById("root")).render(jsx);
那我們就來實現一下該createRoot和render方法
源碼中的render是掛載到了原型對象上
// react-dom
import createFiber from "./ReactFiber";
import { scheduleUpdateOnFiber } from "./ReactFiberWorkLoop";// 構造函數
function ReactDOMRoot(internalRoot) {this._internalRoot = internalRoot;
}ReactDOMRoot.prototype.render = function (children) {// 最原始的vnode節點(jsx) 我們需要的是fiber結構的vnodeconst root = this._internalRoot;// 原生dom節點console.log(root, "root");updateContainer(children, root);
};// 初次渲染 組件到g根dom節點上
function updateContainer(element, container) {const { containerInfo } = container;const fiber = createFiber(element, {type: containerInfo.nodeName.toLocaleLowerCase(),stateNode: containerInfo,});// 組件初次渲染scheduleUpdateOnFiber(fiber);
}
function createRoot(container) {const root = { containerInfo: container };return new ReactDOMRoot(root);
}// 一整個文件是ReactDOM, createRoot是ReactDOM上的一個方法
export default { createRoot };
scheduleUpdateOnFiber方法實現
觸發任務調度方法,來執行fiber的生成performUnitOfWork和commit提交兩個步驟
scheduleCallback是借助了MessageChannel方法來從最小堆中取優先級最高的任務來執行,此處暫時表示執行workLoop方法
// import scheduleCallback from '...todo'
export function scheduleUpdateOnFiber(fiber) {wip = fiber;wipRoot = fiber;scheduleCallback(workLoop);// scheduleCallback(() => {// console.log("scheduleCallback1");// });// scheduleCallback(() => {// console.log("scheduleCallback2");// });// scheduleCallback(() => {// console.log("scheduleCallback3");// });// scheduleCallback(() => {// console.log("scheduleCallbac4");// });
}function workLoop() {//協調while (wip) {performUnitOfWork();}//提交if (!wip && wipRoot) {commitWork();}
}
- 根據最原始的 vnode 節點(jsx) 調用 createFiber 方法生成我們需要的 fiber 結構的 vnode
這一塊已經實現了
const fiber = createFiber(element, {type: containerInfo.nodeName.toLocaleLowerCase(),stateNode: containerInfo,});
- 根據 fiber 上不同 tag 屬性調用不同的 fiber 渲染方法 該方法里面調用了 reconcileChildren 方法(協調 children 生成 fiber 鏈表) 遞歸生成 fiber 單鏈表結構
以函數組件為例:
export function updateFunctionComponent(wip) {renderWithHooks(wip);// 函數組件的type是個函數 直接執行拿到childrenconst { type, props } = wip;// 子節點const children = type(props);reconcileChildren(wip, children);
}
reconcileChildren方法就是協調,協調所有后代節點生成fiber單鏈表結構
// 協調children生成fiber鏈表
export function reconcileChildren(returnFiber, children) {const newChildren = isArray(children) ? children : [children];// old fiber頭節點let oldFiber = returnFiber.alternate?.child;// 為啥去掉這句就不能渲染了 todo ...? 現在不會了 但是會出現兩個相同的元素if (isStringOrNumber(children)) {return;}// 實現fiber的鏈表結構let previousNewFiber = null;let newIndex = 0;for (newIndex = 0; newIndex < newChildren.length; newIndex++) {const newChild = newChildren[newIndex];// 如果newChil為null,會在createFiber中報錯if (newChild === null) {continue;}const newFiber = createFiber(newChild, returnFiber);const same = sameNode(newFiber, oldFiber);// 更新復用if (same) {Object.assign(newFiber, {stateNode: oldFiber.stateNode,alternate: oldFiber,flags: Update, // 默認是Placement 新增});}if (!same && oldFiber) {// 刪除節點deleteChild(returnFiber, oldFiber);}// ?? todo...if (oldFiber) {oldFiber = oldFiber.sibling;}// 第一個子fiber 好比nexIndex===0if (previousNewFiber === null) {returnFiber.child = newFiber;} else {previousNewFiber.sibling = newFiber;}// 記錄一下上次的fiberpreviousNewFiber = newFiber;}if (newIndex === newChildren.length) {deleteRemainingChildren(returnFiber, oldFiber);return;}
}
-
處理完所有 fiber 和 子 fiber 后,開始往 root 節點里面進行遞歸提交,包括提交自己,第一個子節點,第一個子節點的兄弟節點(增刪改查)的操作 調用了 commitRoot(commitWork)方法
-
根據 flags 屬性來判斷是新增 還是更新 還是刪除
- 新增則調用 dom 元素的 appendChild 方法
- 更新則根據新老節點對比 調用 updateNode 方法
- 刪除則調用 commitDeletion 通過 removeChild(父 dom 和子 dom)來刪除
function commitWork(wip) {if (!wip) {return false;}// 1.更新自己const { flags, stateNode, type } = wip;// 追加if (flags & Placement && stateNode) {// 函數組件prop.children的父級是函數組件名 再往上就是root根節點// const parentNode = wip.return.stateNode;const parentNode = getParentNode(wip.return);parentNode.appendChild(stateNode);}// 更新if (flags & Update && stateNode) {updateNode(stateNode, wip.alternate.props, wip.props);}// 刪除if (wip.deletions) {// 通過父節點來刪除commitDeletion(wip.deletions, stateNode || parentNode);}// 2.更新子節點commitWork(wip.child);// 3.更新兄弟節點commitWork(wip.sibling);
}
- 初始化結束
更新(更新操作無非就是 useState,useReducer 等改變了組件狀態而導致更新)
所以在 hook 函數里 我們需要去調用 scheduleUpdateOnFiber 方法來出觸發組件更新
然后回到了上面初次渲染一樣的邏輯