源码中定义了不同类型节点的枚举值
组件类型
- 文本节点
- 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 方法来出触发组件更新
然后回到了上面初次渲染一样的逻辑