react学习笔记-2
react学习笔记-2
此处接着 https://react.iamkasong.com/process/reconciler.html 继续分析
更新流程
先看两个更新方法:
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && /** 需要让出线程了 */ !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
这里也解释了上一张为什么时间切片的调用没法实现的原因,他调用的是 workLoopSync,而不是 Concurrent
workInProgress 代表当前已创建的 workInProgress fiber,这是一个全局变量。
performUnitOfWork 方法会创建下一个 Fiber 节点并赋值给 workInProgress,并将 workInProgress 与已创建的 Fiber 节点连接起来构成 Fiber 树。
接下来看看 performUnitOfWork
干了啥
function performUnitOfWork(unitOfWork: Fiber): void {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
// 当前屏幕上的 fiber 节点
const current = unitOfWork.alternate;
let next;
// 为每个遍历到的节点调用 beginWork 方法
// 该方法具体作用看下文,next 代表当前节点的子节点
next = beginWork(/* 当前屏幕上的节点 */ current, /* 接下来要更新的节点 */ unitOfWork, entangledRenderLanes);
// ...
// 更新 props
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
// 没有子节点了,去检查当前节点是否存在 sibling,存在的话就让 sibling 进入 beginWork 阶段
completeUnitOfWork(unitOfWork);
} else {
// 继续递归,回到 workLoop
workInProgress = next;
}
}
其中:
beginWork
用于决定该组件是否要被更新,并执行组件的 render 方法completeUnitOfWork
completeWork
用于创建、更新、删除 DOM 节点,以及调用组件生命周期方法等
看到这里我们可以知道,React 的中断发生时刻位于每个 Fiber 节点对比更新前的时候,而在执行 beginWork 与 completeWork 这段时间则是无法分割操作。
什么嘛,我还以为是像操作系统能从寄存器和时钟中断层面实现的更新打断(逃
更新流程例子
虽然简单但是非常重要,他与一般的dfs在实现上稍微有点区别
https://react.iamkasong.com/process/reconciler.html#%E4%BE%8B%E5%AD%90
手动实现一个可中断的 React 更新流程
个人依照理解实现的
参照着源码写了一份,个人不理解的地方就是回溯的时候是怎么防止死循环的,个人想到的就只有在 compeleteWork 那里来确认节点是否被访问过,但是这样会多出一次节点访问,影响效率
运行环境是 node
function randomUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0
const v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
}
class FiberNode {
/** @type {FiberNode} */
sibling
/** @type {FiberNode} */
return
/** @type {FiberNode} */
child
visited = false
uuid
constructor({
parent = null,
sibling = null,
child = null
} = {}) {
this.return = parent
this.sibling = sibling
this.child = child
this.uuid = randomUUID()
}
}
/** **Becareful to use!!!** */
function createAFiberNode(layerCount = 5, eachLayerObjCount = 10) {
const root = new FiberNode()
root._isRoot = true
root.child = new FiberNode()
const dfs = (node, parent, layer = 0) => {
node.return = parent
if (layer === layerCount) {
return
}
let currentNode = node
for (let i = 0; i < eachLayerObjCount; i++) {
currentNode.sibling = new FiberNode()
currentNode.sibling.return = parent
currentNode.child = new FiberNode()
dfs(currentNode.child, currentNode, layer + 1)
currentNode = currentNode.sibling
}
}
dfs(root.child, root)
return root
}
let workingInProgress = null
function beginWork(node) {
// just dfs to child
// here use diff algorithm
return node?.child || null
}
function completeWork(node) {
// just dfs to child
// 源码里头似乎都是 return null,应该就是专门渲染节点,通常情况下不做其他返回内容
return null
}
function completeUnitOfWork(/** @type { FiberNode } */ node) {
let completedWork = node
do {
const returnFiber = completedWork.return;
let next = completeWork(completedWork);
if (next !== null) {
workingInProgress = next
return
}
const siblingFiber = completedWork.sibling
if (siblingFiber !== null) {
workingInProgress = siblingFiber
return
}
completedWork = returnFiber
workingInProgress = completedWork
} while (completedWork !== null);
// We've reached the root.
}
function goToUnitOfWork(node) {
let next = beginWork(node)
if (next !== null) {
workingInProgress = next
} else {
completeUnitOfWork(node)
}
}
let startTime = +Date.now();
function createNeedYield(duration = 1000) {
return () => {
const now = +Date.now();
const timer_update = now - startTime > duration
if (timer_update) {
startTime = now
}
return timer_update
}
}
const needYield = createNeedYield(10)
function workLoopSync() {
const taskBeginTime = +Date.now();
while (workingInProgress !== null) {
workingInProgress.visited = true
goToUnitOfWork(workingInProgress)
}
console.log(`Sync interrupt workLoop, spend ${+Date.now() - taskBeginTime}ms, current work progress is ${workingInProgress?.uuid || 'null'}`);
}
function workLoopConcurrent() {
const taskBeginTime = +Date.now();
while (workingInProgress !== null && !needYield()) {
workingInProgress.visited = true
goToUnitOfWork(workingInProgress)
}
console.log(`Concurrent interrupt workLoop, spend ${+Date.now() - taskBeginTime}ms, current work progress is ${workingInProgress?.uuid || 'null'}`);
}
function runWorkLoop() {
startTime = +Date.now();
workLoopConcurrent()
}
function printTree(node) {
if (!node) return
console.log('node', node.tag, node.key, node.child)
if (node.child) {
printTree(node.child)
}
if (node.sibling) {
printTree(node.sibling)
}
}
function start() {
workingInProgress = createAFiberNode(5, 10)
// workingInProgress = createAFiberNode(2, 2)
let root = workingInProgress
console.log('begin workLoop');
runWorkLoop()
let timer = setInterval(() => {
if (workingInProgress === null) {
clearInterval(timer)
console.log('end workLoop');
return
}
runWorkLoop()
console.log('sleep for 1s');
}, 1000);
}
function startSync() {
workingInProgress = createAFiberNode(5, 10)
console.log('begin workLoop');
workLoopSync()
}
start()
beginWork 解析
直接看源码吧,此处为 1.18 的
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
// ...各种对比方法用于确认该节点相比 workInProgress 是否发生了更新
// 即 update
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
// 上下文变化 + props 变化
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
// 开发环境转为类型变化,进行强制更新
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// 假如发生了变化,则需要进行完整更新
didReceiveUpdate = true;
} else {
// Neither props nor legacy context changes. Check if there's a pending
// update or context change.
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);
if (
!hasScheduledUpdateOrContext &&
// If this is the second pass of an error or suspense boundary, there
// may not be work scheduled on `current`, so we check for this flag.
(workInProgress.flags & DidCapture) === NoFlags
) {
// No pending updates or context. Bail out now.
didReceiveUpdate = false;
// 复用 current
return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes);
}
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// This is a special case that only exists for legacy mode.
didReceiveUpdate = true;
} else {
// An update was scheduled on this fiber, but there are no new props
// nor legacy context. Set this to false. If an update queue or context
// consumer produces a changed value, it will set this to true. Otherwise,
// the component will assume the children have not changed and bail out.
didReceiveUpdate = false;
}
}
} else {
// 此处无节点,即为 mount
// ...新进入的节点
didReceiveUpdate = false;
// 水合判断 + 拷贝 fiber 判断
if (getIsHydrating() && isForkedChild(workInProgress)) {}
}
// 设置更新优先级为无
workInProgress.lanes = NoLanes;
// 根据当前 fiber 的不同类型来走不同的 挂载/更新 流程
switch (workInProgress.tag) {
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
disableDefaultPropsExceptForClasses ||
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case ClassComponent:
// ...
}
}
对于我们常见的组件类型,如 FunctionComponent/ClassComponent/HostComponent
,最终会进入 reconcileChildren
方法。
reconcileChildren
1.18 源码:
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
// mount
if (current === null) {
// If this is a fresh new component that hasn't been rendered yet, we
// won't update its child set by applying minimal side-effects. Instead,
// we will add them all to the child before it gets rendered. That means
// we can optimize this reconciliation pass by not tracking side-effects.
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
}
// update,使用 diff 算法
else {
// If the current child is the same as the work in progress, it means that
// we haven't yet started any work on these children. Therefore, we use
// the clone algorithm to create a copy of all the current children.
// If we had any progressed work already, that is invalid at this point so
// let's throw it out.
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
byd 上面都解释的很清楚了,这个方法最终都会生成一个新的 fiber 节点并作为 beginWork 的返回值,然后赋值给 workInProgress,即下一个工作单元的目标对象
effectTag
我们知道,render 阶段的工作是在内存中进行,当工作结束后会通知 Renderer 需要执行的 DOM 操作。要执行DOM操作的具体类型就保存在 fiber.effectTag 中。 比如:
// DOM需要插入到页面中
export const Placement = /* */ 0b00000000000010;
// DOM需要更新
export const Update = /* */ 0b00000000000100;
那么,如果要通知Renderer将Fiber节点对应的DOM节点插入页面中,需要满足两个条件:
fiber.stateNode
存在,即Fiber节点中保存了对应的DOM节点(fiber.effectTag & Placement) !== 0
,即Fiber节点存在Placement effectTag
我们知道,mount时,fiber.stateNode === null
,且在reconcileChildren中调用的mountChildFibers不会为Fiber节点赋值effectTag。那么首屏渲染如何完成呢?
针对第一个问题,fiber.stateNode会在completeWork中创建,我们会在下一节介绍。
第二个问题的答案十分巧妙:假设mountChildFibers也会赋值effectTag,那么可以预见mount时整棵Fiber树所有节点都会有Placement effectTag。那么commit阶段在执行DOM操作时每个节点都会执行一次插入操作,这样大量的DOM操作是极低效的。
为了解决这个问题,在mount时只有rootFiber会赋值Placement effectTag,在commit阶段只会执行一次插入操作。
completeWork 解析
completeUnitOfWork
缩略源码
function completeUnitOfWork(unitOfWork: Fiber): void {
// Attempt to complete the current unit of work, then move to the next
// sibling. If there are no more siblings, return to the parent fiber.
let completedWork: Fiber = unitOfWork;
do {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
const current = completedWork.alternate;
// parent 节点
const returnFiber = completedWork.return;
let next;
// 处理已完成更新的 fiber 节点
next = completeWork(current, completedWork, entangledRenderLanes);
// 还有子节点更新,继续
if (next !== null) {
// Completing this fiber spawned new work. Work on that next.
workInProgress = next;
return;
}
// 检查 sibling,有就继续走 beginWork
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
workInProgress = siblingFiber;
return;
}
// Otherwise, return to the parent
// $FlowFixMe[incompatible-type] we bail out when we get a null
completedWork = returnFiber;
// Update the next thing we're working on in case something throws.
workInProgress = completedWork;
} while (completedWork !== null);
// We've reached the root.
if (workInProgressRootExitStatus === RootInProgress) {
workInProgressRootExitStatus = RootCompleted;
}
}
completeWork
缩略源码
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
// Note: This intentionally doesn't check if we're hydrating because comparing
// to the current tree provider fiber is just as fast and less error-prone.
// Ideally we would have a special version of the work loop only
// for hydration.
popTreeContext(workInProgress);
//
switch (workInProgress.tag) {
case IncompleteFunctionComponent: {
if (disableLegacyMode) {
break;
}
// Fallthrough
}
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
bubbleProperties(workInProgress);
return null;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
bubbleProperties(workInProgress);
return null;
}
case HostComponent: {
// ...省略
return null;
}
}
throw new Error(
`Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
'React. Please file an issue.',
);
}
从 HostComponent 看起
这里是 原生DOM 对应 FiberNode 的一层,其他的另外再说(
以下所有源码均来自 React18
case HostComponent: {
popHostContext(workInProgress);
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
// update
} else {
// mount
}
bubbleProperties(workInProgress);
// This must come at the very end of the complete phase, because it might
// throw to suspend, and if the resource immediately loads, the work loop
// will resume rendering as if the work-in-progress completed. So it must
// fully complete.
preloadInstanceAndSuspendIfNeeded(
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
renderLanes,
);
return null;
}
HostComponent update
似乎没什么好说的,这里解释一下 updateHostComponent
里面干了啥:
- 浅比较 props,确认是否要更新
- 调用外部的渲染器方法创建实例对象
- 调用渲染器方法,处理实例:
- onClick、onChange等回调函数的注册
- 处理style prop
- 处理DANGEROUSLY_SET_INNER_HTML prop
- 处理children prop
PS:这里与网站写的不同,因为这里只关注了协调器本身,网站原文是涉及到了渲染器,以及队列更新过程,但在 18 这些部分似乎都放到了其他地方
updateHostComponent(
current,
workInProgress,
type,
newProps,
renderLanes,
);
后续的 updateQueue 在 18 也找不到了,只在 SuspenseComponent 看到过
16 则没有把这些进行分离
HostComponent mount
mount 逻辑:
// ...渲染前判断有没有 newProps
const currentHostContext = getHostContext();
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// hydrate 准备
} else {
const rootContainerInstance = getRootHostContainer();
// 根据节点调用渲染器方法创建实例
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 将新的child DOM 节点插入到生成的实例中
appendAllChildren(instance, workInProgress, false, false);
// 赋值给 stateNode
workInProgress.stateNode = instance;
// 调用渲染器方法处理实例
if (
finalizeInitialChildren(
instance,
type,
newProps,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
}
回到前文 mount时只会在rootFiber存在Placement effectTag。那么commit阶段是如何通过一次插入DOM操作(对应一个Placement effectTag)将整棵DOM树插入页面的呢?
原因就在于completeWork中的appendAllChildren方法。
由于completeWork属于“归”阶段调用的函数,每次调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下。那么当“归”到rootFiber时,我们已经有一个构建好的离屏DOM树。
即 react 自底向上回溯时,会在内存里从底层一步步创建元素,并向更高层的 DOM 元素插入进去,这样就做到了不需要更新用户视图也能创建 / 更新 DOM,当回到根元素时,我们就有了一个完整的 DOM 树。
effectList(已被移除?)
前话:作者在写本部分时已经开始懵逼了,考试 + 课设让我把整个 react 基本结构都给忘了,所以写到这里可能质量不是很高。
此部分在 react 18 与参考文章已不同,源码内已经找不到 effectList 相关内容,即判断 completedWork.effectTag & Incomplete) === NoEffect
已不存在
后面笔者看 commit 阶段时会回来填坑
Last
到此处就完成离屏创建新的 fiber 流程了,接下来就是 commit,将其渲染到屏幕上,在 renderRootSync
内的 work loop 和状态判断结束后,调用 commitRoot
方法,开启渲染流程。