源码中定义了不同类型节点的枚举值
组件类型
- 文本节点
- 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, // 第一个子fiber
sibling: null, // 下一个兄弟fiber
return: returnFiber, // 父节点
// 标记节点是什么类型的
flags: Placement,
deletions: null, // 要删除子节点 null或者[]
index: null, //当前层级下的下标,从0开始
// 记录上一次的状态 函数组件和类组件不一样
memorizedState: null,
// old fiber
alternate: 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 a
case 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结构的vnode
const 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是个函数 直接执行拿到children
const { 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===0
if (previousNewFiber === null) {
returnFiber.child = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
// 记录一下上次的fiber
previousNewFiber = 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 方法来出触发组件更新
然后回到了上面初次渲染一样的逻辑