开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情
【学习笔记】React18源码学习(一) 设计理念与Scheduler
0. 工作周期
renderRootSync
设置渲染的上下文,为工作准备一个新的堆栈,然后进入一个循环,在循环中通过重复调用workLoopSync函数来处理工作。如果在此循环中抛出错误,则会被handleThrow函数捕获和处理。
outer: do {
try {
if (
workInProgressSuspendedReason !== NotSuspended &&
workInProgress !== null
) {
// 工作循环被挂起。在同步渲染期间,我们不会让出主线程。
const unitOfWork = workInProgress;
const thrownValue = workInProgressThrownValue;
switch (workInProgressSuspendedReason) {
case SuspendedOnHydration: {
// 一个更新流入了一个未注水的树。
// 中断当前渲染,以便workLoop可以切换到注水的模式。
resetWorkInProgressStack();
workInProgressRootExitStatus = RootDidNotComplete;
break outer;
}
default:
workInProgressSuspendedReason = NotSuspended;
workInProgressThrownValue = null;
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
break;
}
}
}
// 开始Work Loop
workLoopSync();
break;
} catch (thrownValue) {
handleThrow(root, thrownValue);
}
} while (true);
workLoopSync
function workLoopSync() {
// Perform work without checking if we need to yield between fiber.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
如果存在WorkInProgress 就执行具体工作,
performUnitOfWork
在Fiber树中每一个单独的节点都可以当作一个工作单元,performUnitOfWork 用于执行工作单元,并按照优先级顺序执行这些工作单元,从而实现协调渲染。
performUnitOfWork() 函数的主要作用是处理当前工作单元,并返回下一个工作单元。其实现通常涉及以下步骤:
- 处理当前工作单元的任务,例如调用组件的生命周期方法、处理事件等。
- 检查是否需要中断当前工作单元的执行(例如,在有更高优先级的工作单元需要执行时)。
- 如果当前工作单元有子节点,则将第一个子节点作为下一个工作单元返回。
- 否则,如果当前工作单元有兄弟节点,则将下一个兄弟节点作为下一个工作单元返回。
- 否则,回溯到父节点,查找有兄弟节点的祖先节点,并将下一个兄弟节点作为下一个工作单元返回。
- 如果没有找到下一个工作单元,则表示整个工作单元链表执行完成。
需要注意的是,performUnitOfWork() 函数在执行期间可能会被多次调用,因为中断和恢复工作单元的执行是 React Fiber 的核心机制之一。在每次中断和恢复执行时,performUnitOfWork() 函数都会被调用,并根据需要返回下一个工作单元。
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, renderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
// 这里开始深度优先遍历
next = beginWork(current, unitOfWork, renderLanes);
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
//
completeUnitOfWork(unitOfWork);
} else {
// 这里的全局变量
workInProgress = next;
// 因 `renderRootSync`里的循环有个判断 workInProgress !== null
// 所以会一直调用beginWork,直到当前节点没有子节点
}
ReactCurrentOwner.current = null;
}
首屏渲染的示例代码
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
</header>
</div>
);
}
套到上面的代码里逻辑如下
root节点进入beginWorkfunction Component App()进入beginWorkdiv节点进入beginWorkheader节点进入beginWorkimg节点进入beginWorkimg没有子节点 进入completeWorkimg兄弟节点p进入beginWork- 文件节点
Edit进入beginWork code进入beginWorkcode进入completeWork,ps 当子节点只有一个文本节点时会直接进入completeworkand save to reload.进入beginWorkand save to reload.进入completeWork下面就用上面的步骤看下他们是怎么串起来的
1. root节点进入beginWork`
1.1 beginWork
// 如果 `didReceiveUpdate` 被标记为 true,那么组件将会更新;否则,组件将不会更新。
didReceiveUpdate = false;
// 由于root节点的tag是3
switch (workInProgress.tag) {
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
}
1.2 updateHostRoot
function updateHostRoot(current, workInProgress, renderLanes) {
// 上下文
pushHostRootContext(workInProgress);
var nextProps = workInProgress.pendingProps;
var prevState = workInProgress.memoizedState;
var prevChildren = prevState.element;
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
var nextState = workInProgress.memoizedState;
var root = workInProgress.stateNode;
var nextChildren = nextState.element;
// 生成子节点的fiber
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
1.3 reconcileChildren
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
if (current === null) {
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
} else {
// 当前是root 节点 current !==null
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
}
}
1.4 reconcileChildFibers
// returnFiber 当前root fiber 节点
// newChild = {type:f App(),$$typeof: Symbol(react.element),[[Prototype]]:Object}
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
// 进入的这里
return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
}
}
1.5 reconcileSingleElement
// element 为 f app() 节点
function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
if (element.type === REACT_FRAGMENT_TYPE) {
var created = createFiberFromFragment(element.props.children, returnFiber.mode, lanes, element.key);
created.return = returnFiber;
return created;
} else {
var _created4 = createFiberFromElement(element, returnFiber.mode, lanes);
_created4.ref = coerceRef(returnFiber, currentFirstChild, element);
_created4.return = returnFiber;
return _created4;
}
}
1.6 createFiberFromElement
function createFiberFromElement(element, mode, lanes) {
const fiber = createFiberFromTypeAndProps(
type,
key,
pendingProps,
owner,
mode,
lanes,
);
return fiber;
}
最终返回了一个子节点的fiber
2. 当 header 节点进入beginWork时
2.1 beginWork
由于header是HostComponent 所以进入 updateHostComponent,当前的current是null,renderLanes 为32
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
2.2 updateHostComponent
这里做了判断当子节点只有一个child并且是纯文本节点,那么nextChildren 会设为null,这样子节点就会直接进入completeWork
function updateHostComponent(current, workInProgress, renderLanes) {
// 判断
const isDirectTextChild = shouldSetTextContent(type, nextProps);
if (isDirectTextChild) {
nextChildren = null;
} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
workInProgress.flags |= ContentReset;
}
// 生成Children
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
这里的reconcileChildren跟上面1.3的一样
2.3 ReconcileChildFibers
// newChild 是个数组
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
if (isArray(newChild)) {
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
}
}
2.4 reconcileChildrenArray
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<any>,
lanes: Lanes,
): Fiber | null {
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
// 从这里进入创建新fiber的逻辑
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
// 插入fiber树
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
// 把第一个子节点return出去,进入新一轮的workloop
return resultingFirstChild;
}
2.5 createChild
createChild同1.6里的 createFiberFromElement
3. 当 code 节点进入 beginWork
3.1 updateHostComponent
前面的过程不再赘述,从 updateHostComponent 这个方法里,有个const isDirectTextChild = shouldSetTextContent(type, nextProps);,因为code只有一个字节点并且是纯文本,所以为true。然后next会被设为null,进入performUnitOfWork
// 简化一下
let next;
// 综上所述当unitOfWork是code时 返回null
next = beginWork(current, unitOfWork, renderLanes);
if (next === null) {
// 进入complete
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
3.2 completeUnitOfWork
大致工作跟performUnitOfWork 一致 参照0.3章节
function completeUnitOfWork(unitOfWork) {
do {
// ...省略前面的赋值
next = completeWork(current, completedWork, renderLanes);
if (next !== null) {
// Completing this fiber spawned new work. Work on that next.
workInProgress = next;
return;
} else {
// ...省略后面的跟当前code不进入的逻辑
}
} while (completedWork !== null)
}
3.3 completeWork
工作流程如下:
- 首先,检查当前
fiber节点的 tag 类型,判断该节点是否为 DOM 元素、文本节点、类组件、函数组件、HostPortal 或者 Suspense 等类型。 - 然后,对于不同的节点类型,执行相应的操作,例如更新 DOM 元素的属性、将文本内容插入到父节点中、调用类组件的 componentWillUnmount 或者 useEffect 等副作用钩子函数。
- 接下来,如果当前节点有兄弟节点,则将兄弟节点添加到父节点的 child 属性中。
- 如果当前节点没有兄弟节点,则遍历当前节点的父节点的祖先节点,找到下一个有兄弟节点的节点,并将其添加到该节点的 child 属性中。如果遍历到了根节点仍然没有找到有兄弟节点的节点,则表明整个树已经遍历完成。
- 最后,如果当前节点是类组件或函数组件,则更新它的 effect list 和 effect tag,以便在 commit 阶段执行相关的副作用操作。
function completeWork(current,workInProgress,lanes) {
case HostComponent: {
popHostContext(workInProgress);
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
// 省略
} else {
const currentHostContext = getHostContext();
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// 注水 ssr 相关
} else {
const rootContainerInstance = getRootHostContainer();
// 创建DOM元素
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 挂载所有子节点
appendAllChildren(instance, workInProgress, false, false);
// 将实例存储在stateNode中
workInProgress.stateNode = instance;
// 某些渲染器需要在提交阶段应用效果以进行初始挂载。
// (例如,DOM渲染器支持对某些元素进行自动聚焦)。
// 确保这样的渲染器被安排在稍后进行工作。
if (
// 设置DOM属性
finalizeInitialChildren(
instance,
type,
newProps,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
bubbleProperties(workInProgress);
return null;
}
}
3.4 createInstance
创建dom元素
function createInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
):Instance {
// 通过createElement 方法创建DOM元素
const domElement: Instance = createElement(
type,
props,
rootContainerInstance,
parentNamespace,
);
// 缓存
precacheFiberNode(internalInstanceHandle, domElement);
updateFiberProps(domElement, props);
return domElement;
}
3.5 finalizeInitialChildren
设置属性
function finalizeInitialChildren(
domElement: Instance,
type: string,
props: Props,
hostContext: HostContext,
): boolean {
setInitialProperties(domElement, type, props);
switch (type) {
case 'button':
case 'input':
case 'select':
case 'textarea':
return !!props.autoFocus;
case 'img':
return true;
default:
return false;
}
}
3.6 setInitialProperties
function setInitialProperties(
domElement: Element,
tag: string,
rawProps: Object,
): void {
// 一推case 放个input的例子
case 'input':
ReactDOMInputInitWrapperState(domElement, rawProps);
props = ReactDOMInputGetHostProps(domElement, rawProps);
// 我们监听这个事件,以确保模拟冒泡的监听器
// 仍然会为无效事件触发。
listenToNonDelegatedEvent('invalid', domElement);
break;
}