一文带你领略react的调度更新机制
简介
- 根据不同的组件更新方式产生新的reactElement对象,根据reactElement对象构建需要更新的fiber树
- 根据构建的fiber树配合渲染器(Renderer)创建视图实例跟渲染更新操作
根据两个工作流程的不同,前者被称为 render阶段,后者被称为commit阶段
类比我们在写代码,先进行代码编写(render),写完之后进行代码提交(commit)
调用入口
无论是初次渲染,还是调度更新,最终都会进入到scheduleUpdateOnFiber的入口逻辑,才正式开始更新/构建的流程 ,可以在ReactDom.render 上打个断点看看
function App() {
return <div onClick={plus}>this is app text </div>;
}
ReactDOM.render(<App />, document.getElementById("root"));
export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
// 省略其他的逻辑
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
performSyncWorkOnRoot(root);
}
render阶段之前
在进入render阶段之前,也就是在调用workLoopSync之前,都会调用prepareFreshStack(root, lanes);刷新栈帧
function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
root.finishedWork = null;
root.finishedLanes = NoLanes;
workInProgressRoot = root;
workInProgress = createWorkInProgress(root.current, null);
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootIncomplete;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
}
更新workInProgressRootRenderLanes,subtreeRenderLanes,workInProgressRootIncludedLanes的车道(后面优化有用),对于ReactDOM.render使用的是syncLane的车道
调用createWorkInProgress(rootFiber,null)构建了新的HostRootFiber,拥有current rootFiber的所有属性(进行了浅克隆), 并将workInProgress指针指向该fiber
Render阶段概述
在初次渲染或者每次触发更新,都会进入render阶段 ,在这个过程,performUnitOfWork函数会通过 深度优先遍历 的方式,根据父fiber进行子fiber的派生,直到所有子孙fiber派生完毕,也就构建出整个棵fiber树了
流程概述
// 我们正在用的ReactDOM.render最终会调用workLoopSync
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
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.
const current = unitOfWork.alternate;
// 对指针workInProgress指向的fiber 派生child fiber
let next = beginWork(current, unitOfWork, subtreeRenderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
// 没有child fibr需要派生,则该fibr进行completeWork
completeUnitOfWork(unitOfWork);
} else {
// 将派生的child fiber赋值给workInProgress 指针
// 继续调用performUnitOfWork, 又进入beginWork派生child fiber
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
- performUnitOfWork(workInProgress),调用beginWork(current,workInProgress)派生child fiber,在这个过程中可能会为fiber增加副作用,尝试更新workInProgress指针
- 当该fiber有child fiber派生,会将workInProgress指向该child fiber,重新进入步骤1
- 当该fiber没有child fiber派生,将fiber赋值给completedWork,调用completeWork函数,为该fiber创建/更新对应的dom节点/属性,可能会为fiber增加副作用
- 该fiber完成completeWork之后,会向上移交effectList,如果fiber本身有副作用,那么该fiber会加入effectList(副作用链表)
- 如果该fiber还有兄弟fiber ,那么将workInProgress指向兄弟fiber,重新进入步骤1:performUnitOfWork(workInProgress),
- 如果该fiber没有兄弟fiber,则fiber的父级fiber赋值给completedWork, 调用completeWork,进入completeWork逻辑,重复进行步骤3,直到根fiber,也就是HostRootFiber
副作用概念
在构建或者更新fiber过程中,可能会为fiber打上各种标记,也就是赋值fiber.flags属性,在后面的commit阶段中,要针对fiber.flags进行处理,如对于class fiber / function component fiber 是否需要调用生命周期方法|hook, 对host fiber,如div fiber,是否需要更新/删除/插入 等等。
在react里面,打标记这种行为被称为增加副作用,具备各种副作用的fiber,在构建fiber的过程中会被串成一条链表,这条链表被称为 effectList
深入理解 Fiber构建
示例代码
function Content() {
const [flag, setFlag] = useState(false);
return (
<>
<p>1</p>
<p>2</p>
<button onClick={() => setFlag(true)}>show</button>
{flag ? <p>3</p> : null}
</>
);
}
class App extends React.Component {
componentDidUpdate(...args) {
console.log("componentDidUpdate", ...args);
}
componentDidMount(...args) {
console.log("componentDidMount", ...args);
}
render() {
return (
<div className="app">
<header>this is header text</header>
<Content />
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
mount阶段:
- 首次应用渲染执行ReactDOM.render创建了FiberRootNode和HostRootFiber节点【后续简称rootFiber】,fiberRootNode是整个react应用的根节点,rootFiber是fiber树的根节点,rootNode跟rootFiber通过current跟stateNode互相连接, 最终触发schedulerUpdateOnFiber,进入render阶段
- render阶段之前会调用prepareFreshStack(root, lanes)刷新栈帧, 创建rootFiber的alternate fiber节点,并将workInProgress指针指向rootFiber
- render阶段会持续调用performUnitOfWork(workInProgress)派生child Fiber
- 当前workInProgress指针指向rootFiber,next=beginWork(hostRootFiber)调用updateHostRoot方法,根据ReactDOM.render传递的 reactElement(App), 调用reconcileChildren(current hostRootFiber, hostRootFiber, reactElement(App), syncLanes)派生 App Fiber,赋值给rootFiber.child并返回App Fiber,next为App Fiber,不为null,移动workInProgress指针指向App Fiber,由于current hostRootFiber 跟workInProgress hostRootFiber都存在,因此进入的是reconcileChildFibers的逻辑,shouldTrackSideEffects为true,最终在派生Fiber()的时候,会赋值 AppFiber.flags = Placement;
export const reconcileChildFibers = ChildReconciler(true);
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
function placeSingleChild(newFiber: Fiber): Fiber {
// This is simpler for the single child case. We only need to do a
// placement for inserting new children.
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags = Placement;
}
return newFiber;
}
- 当前workInProgress指针指向App Fiber,执行next=beginWork(App Fiber),由于是mount阶段执行updateClassComponent ,由于是mount阶段,调用constructClassInstance构造了App class instance, 调用instance.render()方法返回对应的reactElement(div),props结构为: {className: 'app', children: [reactElement(header), reactElement(Content)]}, 接着调用 reconcileChildren(null, App Fiber, reactElement(div), syncLanes), 然后派生出子FIber:div Fiber,赋值给App Fiber的child,并返回给next,next为div Fiber,不为null,则移动workInProgress指针指向它。
- 当前workInProgress指针指向div Fiber,next=beginWork(div Fiber),调用updateHostComponent,执行reconcileChildren(null, div Fiber, [reactElement(header), reactElement(Content)], syncLanes); 由于传递的children是数组,会调用reconcileChildrenArray,对传递的reactElement(header), reactElement(Content)依次调用createFiber构建fiber,兄弟fiber之间通过sibling链接,通过return指向父级fiber(div Fiber),并将构建好的第一个fiber:header Fiber, 赋值给workInProgress.child,并返回给next,next不为null,则移动workInProgress指针指向header Fiber
当前workInProgress指针指向header Fiber,next=beginWork(header Fiber),调用了updateHostComponent, 进入isDirectTextChild优化逻辑,调用reconcileChildren(null, headerFiber, null, syncLanes); 赋值的workInProgress.child 为null,next为null
function updateHostComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const type = workInProgress.type;
const nextProps = workInProgress.pendingProps;
const prevProps = current !== null ? current.memoizedProps : null;
let nextChildren = nextProps.children;
const isDirectTextChild = shouldSetTextContent(type, nextProps);
// 优化逻辑,不生成child fiber,仅保存props.children属性
if (isDirectTextChild) {
// We special case a direct text child of a host node. This is a common
// case. We won't handle it as a reified child. We will instead handle
// this in the host environment that also has access to this prop. That
// avoids allocating another HostText fiber and traversing it.
nextChildren = null;
} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
// If we're switching from a direct text child to a normal child, or to
// empty, we need to schedule the text content to be reset.
workInProgress.flags |= ContentReset;
}
markRef(current, workInProgress);
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function shouldSetTextContent(type: string, props: Props): boolean {
return (
type === 'textarea' ||
type === 'option' ||
type === 'noscript' ||
typeof props.children === 'string' ||
typeof props.children === 'number' ||
(typeof props.dangerouslySetInnerHTML === 'object' &&
props.dangerouslySetInnerHTML !== null &&
props.dangerouslySetInnerHTML.__html != null)
);
}
当前workInProgress指向的header Fiber,返回的next为null,调用了completeUnitOfWork(header Fiber)的逻辑
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 = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
// Check if the work completed or if something threw.
if ((completedWork.flags & Incomplete) === NoFlags) {
// 根据fiber节点构建dom实例或者更新属性
let next = completeWork(current, completedWork, subtreeRenderLanes);
if (next !== null) {
// Completing this fiber spawned new work. Work on that next.
workInProgress = next;
return;
}
if (
returnFiber !== null &&
// Do not append effects to parents if a sibling failed to complete
(returnFiber.flags & Incomplete) === NoFlags
) {
// Append all the effects of the subtree and this fiber onto the effect
// list of the parent. The completion order of the children affects the
// side-effect order.
// 每次completeUnitOfWork都将副作用链表上移给父级fiber
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
// If this fiber had side-effects, we append it AFTER the children's
// side-effects. We can perform certain side-effects earlier if needed,
// by doing multiple passes over the effect list. We don't want to
// schedule our own side-effect on our own list because if end up
// reusing children we'll schedule this effect onto itself since we're
// at the end.
const flags = completedWork.flags;
// Skip both NoWork and PerformedWork tags when creating the effect
// list. PerformedWork effect is read by React DevTools but shouldn't be
// committed.
// 如果当前的fiber有副作用需要处理,那么添加到当前effectList最后
if (flags > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
}
// 发现当前的fiber还有兄弟fiber
// 则移动workInProgress指针到该兄弟fiber,重新进入perforUnitOfWork继续派生子fiber
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
workInProgress = siblingFiber;
return;
}
// 如果没有兄弟fiber需要处理,那么移动completedWork 指向 父级fiber,进入completeWork
// Otherwise, return to the parent
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 === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
}
- 此时completedWork指向header Fiber,调用next = completeWork(null, header Fiber, syncLanes);由于是mount阶段,则调用createInstance会创建dom,赋值给header fiber.stateNode,并返回null,赋值给next,此时判断header fiber.sibling 也就是Content Fiber存在,则将workInProgress指针指向Content Fiber,退出completeUnitOfWork流程,重新进入performUnitOfWork
- 此时workInProgress指针指向Content Fiber, 调用next=beginWork(Content Fiber), 由于是mount阶段执行mountIndeterminateComponent ,里面又执行了renderWithHooks, 会执行Content函数,返回了[reactElement(p1),reactElement(p2)]数组,执行reconcileChildren(null, Content Fiber, [reactElement(p1), reactElement(p2)], syncLanes); 由于传递的children是数组,会调用reconcileChildrenArray,对传递的reactElement(p1), reactElement(p2)依次调用createFiber构建fiber,兄弟fiber之间通过sibling链接,通过return指向父级fiber(Content Fiber),并将构建好的第一个p1 Fiber, 赋值给workInProgress.child,并返回给next,next不为null,则移动workInProgress指针指向p1 Fiber
- 此时workInProgress指针指向p1 Fiber, 调用next=beginWork(p1 Fiber), 进入isDirectTextChild优化逻辑,调用reconcileChildren(null, p1 fiber, null, SyncLanes); 赋值的workInProgress.child 为null,返回next为null,进入completeUnitOfWork(p1 Fiber)的逻辑
- 此时completedWork指向p1 Fiber,调用next = completeWork(null, p1 Fiber, SyncLanes);由于是mount阶段,则调用createInstance会创建dom,赋值给p1 fiber.stateNode,并返回null赋值给next,此时判断p1 fiber.sibling 也就是p2 Fiber存在,则将workInProgress指针指向p2 fiber,退出completeUnitOfWork流程,重新进入performUnitOfWork
- 此时workInProgress指针指向p2 Fiber, 调用next=beginWork(p2 Fiber), 进入isDirectTextChild优化逻辑,调用reconcileChildren(null, p2 fiber, null, SyncLanes); 赋值的workInProgress.child 为null,返回next为null,进入completeUnitOfWork(p2 Fiber)的逻辑
- 此时completedWork指向p2 Fiber,调用next = completeWork(null, p2 Fiber, SyncLanes);由于是mount阶段,则调用createInstance会创建dom,赋值给p2 fiber.stateNode,并返回null赋值给next,此时判断p2 fiber.sibling为null,则将completedWork指针指向p2 fiber.return,也就是父级fiberContent Fiber
- 此时completedWork指向Content Fiber,调用next=completeWork(Fiber()),属于IndeterminateComponent不做任何处理返回null, completedWork指向 Fiber().return也就是 Fiber()
- 此时completedWork指向Fiber(), 调用next=completeWork(Fiber()),由于是mount阶段,则调用createInstance会创建dom,赋值给fiber().stateNode,同时将子孙fiber中的dom加入到该dom节点下,并返回null赋值给next, completedWork指向 Fiber().return也就是 Fiber()
- 此时completedWork指向Fiber(), 调用next=completeWork(Fiber()),属于ClassComponent不做任何处理返回null, 发现有Fiber().flags 存在,则将Fiber(加入副作用链表, completedWork指向Fiber()
if (flags > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
- completedWork指向Fiber(), 不需要处理,退出completeUnitofWork,并标记workInProgressRootExitStatus = RootCompleted;
- 下一步进入commit阶段
Update阶段:
// 点击button之后,触发更新后的Content组件是这样子的
function Content() {
const [flag, setFlag] = useState(false);
return (
<>
<p>1</p>
<p>2</p>
<button onClick={() => setFlag(true)}>show</button>
{flag ? <p>3</p> : null}
</>
);
}
class App extends React.Component {
componentDidUpdate(...args) {
console.log("componentDidUpdate", ...args);
}
componentDidMount(...args) {
console.log("componentDidMount", ...args);
}
render() {
return (
<div className="app">
<header>this is header text</header>
<Content />
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
- 用户点击button ,触发react绑定的onClick方法,调用dispatchAction方法,将flag设置为true,触发scheduleUpdateOnFiber更新,在进入render阶段之前,会跟前面一样清空栈帧,然后会调用performSyncWorkOnRoot进入render阶段,主要关注这个函数,会从触发更新的fiber: Fiber() 标记lanes为syncLanes,然后向上去标记祖先fiber.childlanes为syncLanes,方便后续进行优化更新
function markUpdateLaneFromFiberToRoot(
sourceFiber: Fiber,
lane: Lane,
): FiberRoot | null {
// Update the source fiber's lanes
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
let alternate = sourceFiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
// Walk the parent path to the root and update the child expiration time.
let node = sourceFiber;
let parent = sourceFiber.return;
while (parent !== null) {
parent.childLanes = mergeLanes(parent.childLanes, lane);
alternate = parent.alternate;
if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
}
node = parent;
parent = parent.return;
}
if (node.tag === HostRoot) {
const root: FiberRoot = node.stateNode;
return root;
} else {
return null;
}
}
- update阶段的递归流程与mount时类似,只是不会新建dom,对于已经存在的fiber,会去对比前后props是否相等,从而标记更新也就是fiber.flags = Update,对于需要插入的fiber(),则标记Placement
beginWork 做了什么?
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const updateLanes = workInProgress.lanes;
// current如果存在,证明就是update阶段,看是否能够直接复用fiber
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged()
) {
// props 或者context 内容发生了改变
didReceiveUpdate = true;
// 该fiber节点的更新优先级不包含在渲染优先级中,直接跳过
} else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false;
// 当前fiber节点无需更新,调用bailoutOnAlreadyFinishedWork递归检测子节点是否需要更新
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else {
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// 例如调用了class 的this.forceUpdate()
didReceiveUpdate = true;
} else {
didReceiveUpdate = false;
}
}
} else {
didReceiveUpdate = false;
}
// 设置当前fiber优先级
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
// 针对第一次mount时的function component
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
// update 阶段function component
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
// update 阶段的 class component
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
// 根fiber节点
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
// 普通hostComponent类型节点: dom element类型
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
// 普通hosttext类型,dom text类型
return updateHostText(current, workInProgress);
// react.createPortal创建的节点
case HostPortal:
return updatePortalComponent(current, workInProgress, renderLanes);
// React.forwardRef
case ForwardRef: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === type
? unresolvedProps
: resolveDefaultProps(type, unresolvedProps);
return updateForwardRef(
current,
workInProgress,
type,
resolvedProps,
renderLanes,
);
}
// <></>节点 React.Fragment
case Fragment:
return updateFragment(current, workInProgress, renderLanes);
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes);
// React.useMemo
case MemoComponent: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
// Resolve outer props first, then resolve inner props.
let resolvedProps = resolveDefaultProps(type, unresolvedProps);
resolvedProps = resolveDefaultProps(type.type, resolvedProps);
return updateMemoComponent(
current,
workInProgress,
type,
resolvedProps,
updateLanes,
renderLanes,
);
}
case SimpleMemoComponent: {
return updateSimpleMemoComponent(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
updateLanes,
renderLanes,
);
}
// mount阶段的class component
case IncompleteClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return mountIncompleteClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
}
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
// Reuse previous dependencies
workInProgress.dependencies = current.dependencies;
}
markSkippedUpdateLanes(workInProgress.lanes);
// Check if the children have any pending work.
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
// The children don't have any work either. We can skip them.
// TODO: Once we add back resuming, we should check if the children are
// a work-in-progress set. If so, we need to transfer their effects.
return null;
} else {
// This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
}
}
export function cloneChildFibers(
current: Fiber | null,
workInProgress: Fiber,
): void {
if (workInProgress.child === null) {
return;
}
let currentChild = workInProgress.child;
let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);
workInProgress.child = newChild;
newChild.return = workInProgress;
while (currentChild.sibling !== null) {
currentChild = currentChild.sibling;
newChild = newChild.sibling = createWorkInProgress(
currentChild,
currentChild.pendingProps,
);
newChild.return = workInProgress;
}
newChild.sibling = null;
}
- update阶段,会通过一些优化路径来尽可能地复用原来地fiber,markUpdateLaneFromFiberToRoot 从触发更新的fiber向上标记childLanes,则其他没有标记
- 在渲染优先级不够的情况会直接cloneFiber ,不会进入下面的流程分支
- 正常update 跟mount 阶段 都会进入switch(workInProgress.tag)流程分支,进入各自的分支调用生成新的nextChildren,最终调用reconcileChildFibers来生成并返回子fiber
- 在reconcileChildren中,通过current是否为null,判断是否处于mount/update, 在mount阶段会调用 mountChildFibers来新建fiber,在update阶段会调用reconcileChildFibers 通过diff算法尝试复用/新建fiber
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
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,
);
} 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,
);
}
}
- 这里的reconcileChildFibers,mountChildFibers 都是 ChildReconciler 创建返回的函数,区别只是一开始传入的shouldTrackSideEffects不同而已
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
// 这里的true跟false,其实就是标志shouldTrackSideEffects
- 函数会根据一开始的闭包变量shouldTrackSideEffects是否为true,从而决定是否追踪副作用,对于Function组件来说,副作用就是useEffect useLayoutEffect这些hooks调用, 而对HostComponent 也就是dom节点来说,意味着节点的增删改。
- 所以我们能够知道,在update阶段,reconcileChildFibers是会追踪副作用,然后更新 fiber.tags,以便在后续commit阶段,进行追踪调用。
- 问题来了,视图渲染也好,hooks调用也好,都是通过副作用的effectList链表来追踪的,由于我们mount的判断条件就是current fiber不存在,那么mount的时候,就没有增加副作用吗? 没有副作用的话,我们怎么在commit阶段把我们生成好的dom挂载上去?
- 虽然我们看到在mount阶段,传入workInProgress fiber的current fiber都是null, 才进入mountChildFibers, 但是其实第一个workInProgress,也就是当前rootFiber.alternate, 是rootFiber经过prepareFreshStack创建并赋值的, 所以rootFiber.alternate第一次作为workInProgress 的时候并不为null
function prepareFreshStack(root, lanes) {
// 如果有,重置并取消上一次没有完成的更新
root.finishedWork = null;
root.finishedLanes = NoLanes;
var timeoutHandle = root.timeoutHandle;
if (timeoutHandle !== noTimeout) {
// The root previous suspended and scheduled a timeout to commit a fallback
// state. Now that we have additional work, cancel the timeout.
root.timeoutHandle = noTimeout; // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
cancelTimeout(timeoutHandle);
}
if (workInProgress !== null) {
var interruptedWork = workInProgress.return;
while (interruptedWork !== null) {
unwindInterruptedWork(interruptedWork);
interruptedWork = interruptedWork.return;
}
}
workInProgressRoot = root;
workInProgress = createWorkInProgress(root.current, null);
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootIncomplete;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
}
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
- 因此在第一次会进入reconcileChildFibers流程,rootFiber的tag为HostRoot, 会进入updateHostRoot(current, workInProgress, renderLanes);
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
- updateHostRoot会处理当前fiber的处理传入的Update, 也就是updateQueue.pending属性,内容是一个Update 节点 ,payload: App 的reactElement对象
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
var nextState = workInProgress.memoizedState; // Caution: React DevTools currently depends on this property
var nextChildren = nextState.element;
调用了reconcileChildren(current, workInProgress, nextChildren, renderLanes);
currentFiber也存在,会进入 workInProgress.child = reconcileChildFibers(workInProgress, null, nextChildren, renderLanes)(注意,这里会追踪副作用)
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
var isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
var isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
case REACT_PORTAL_TYPE:
return placeSingleChild(reconcileSinglePortal(returnFiber, currentFirstChild, newChild, lanes));
}
}
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, lanes));
}
if (isArray$1(newChild)) {
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
}
// 省略其他兼容处理
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
return reconcileChildFibers;
}
正常情况,nextChildren 会是单个App组件的reactElement对象,因此会进入REACT_ELEMENT_TYPE分支
case REACT_ELEMENT_TYPE:
return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
function placeSingleChild(newFiber) {
// This is simpler for the single child case. We only need to do a
// placement for inserting new children.
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags = Placement;
}
return newFiber;
}
根据 ReactElemen对象 新建对应的App fiber后, shouldTrackSideEffects为true,currentFiber存在,App fiber.current 不存在,满足条件, App Fiber 追加了Placement的副作用,
这里很重要,在后面的completeWork中,负责收集effectList链表的时候, 判断这个fiber被增加了Placement的flags,所以该fiber会被追加到effectList
var flags = completedWork.flags;
// Skip both NoWork and PerformedWork tags when creating the effect list.
// PerformedWork effect is read by React DevTools but shouldn't be committed.
if (flags > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
completework做了什么
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
return null;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
return null;
}
case HostRoot: {
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
resetMutableSourceWorkInProgressVersions();
const fiberRoot = (workInProgress.stateNode: FiberRoot);
if (fiberRoot.pendingContext) {
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
}
if (current === null || current.child === null) {
if (!fiberRoot.hydrate) {
// Schedule an effect to clear this container at the start of the next commit.
// This handles the case of React rendering into a container with previous children.
// It's also safe to do for updates too, because current.child would only be null
// if the previous render was null (so the the container would already be empty).
workInProgress.flags |= Snapshot;
}
}
updateHostContainer(workInProgress);
return null;
}
// 以web为例,如div span 等html 标签元素
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
// update 阶段
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
// mount 阶段
if (!newProps) {
// This can happen when we abort work.
return null;
}
const currentHostContext = getHostContext();
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
return null;
}
// 省略后面的case
}
}
我们重点关注与视图渲染有关的case,主要是HostComponent,在web平台代表了我们平时使用div,p,span等html元素生成的fiber类型,
- 与处理beginWork一样,也是通过current !== null && workInProgress.stateNode != null判断当前处于update/mount
- 当处于mount阶段, 会调用createInstance为Fiber节点生成对应的DOM节点,调用appendAllChildren将子孙DOM节点插入刚生成的DOM节点中,并调用finalizeInitialChildren 根据newProps的属性,初始化当前dom属性,如果有需要更新,则markUpdate当前的fiber,workInProgress.flags |= Update;
- 当处于update阶段,由于之前mount创建dom已经渲染到页面,此时并不需要创建dom,会调用 updateHostComponent,处理传入的props,将其属性经过转换后的updatePayload赋值给workInProgress.updateQueue,最终会在commit阶段被渲染在页面上。其中updatePayload为数组形式,他的偶数索引的值为变化的key,奇数索引的值为变化的value
import {
createInstance,
createTextInstance,
appendInitialChild,
finalizeInitialChildren,
prepareUpdate,
supportsMutation,
supportsPersistence,
cloneInstance,
cloneHiddenInstance,
cloneHiddenTextInstance,
createContainerChildSet,
appendChildToContainerChildSet,
finalizeContainerChildren,
getFundamentalComponentInstance,
mountFundamentalComponent,
cloneFundamentalInstance,
shouldUpdateFundamentalComponent,
preparePortalMount,
prepareScopeUpdate,
} from './ReactFiberHostConfig';
- 这里需要说明的是,无论是createInstance还是updateHostComponent,是根据不同的平台而不同,在web平台就是创建dom节点,在其他平台如RN,就是创建View节点之类的,这里的react代码是通用的,引入的创建与更新方法,在不同平台,会通过不同的映射打包不同的文件,例如在web平台,ReactFiberHostConfig 会引入ReactFiberHostConfig.dom.js, 在RN平台,会引入ReactFiberHostConfig.native.js, 在测试平台,会引入ReactFiberHostConfig.test.js
- 我们会注意到, 在completeWork时,mount 阶段 生成的 dom,会调用appendAllChildren ,递归到更节点的时候,就构成一棵dom树(离屏),在commit 阶段会渲染到视图上,那么在commit阶段,我们是否需要重新遍历一遍新构建的fiber树,做dom的插入吗 ? 并不是的,还记得我们前面beginWork里面提到的追加副作用吗, 当完成每一次completeWork的时候,会去判断当前的fiber节点flags是否存在副作用, 如果有,会添加到effectList 链表中, effectList 是从rootFiber.firstEffect开始的链表,rootFiber.firstEffect -> App Fiber -> ....(假如后面的子fiber有副作用的话,通过next指针依次链接)
- 这样在commit阶段,去遍历effectList执行副作用即可,例如我们的App组件,增加了Placement的副作用,并且此时构建的dom已经是一棵离屏的dom树,最后只需要处理这个副作用,就可以将整棵dom树加到根节点下 div#root
Commit阶段
调用逻辑
function performSyncWorkOnRoot() {
// ...
exitStatus = renderRootSync(root, lanes); // render阶段
// ...
const finishedWork: Fiber = (root.current.alternate: any);
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
commitRoot(root); // commit阶段
}
// 以最高优先级,调度commitRootImpl执行
function commitRoot(root) {
const renderPriorityLevel = getCurrentPriorityLevel();
runWithPriority(
ImmediateSchedulerPriority,
commitRootImpl.bind(null, root, renderPriorityLevel),
);
return null;
}
在render阶段结束后,会将当前的workInProgress跟 renderLanes 树赋值给FiberRootNode的finishedWork 跟 finishedLanes,调用commitRoot(rootFiberNode)进入 Renderer(渲染器)介入的工作流程,也就是commit阶段,细分为三个阶段:
beforeMutation(渲染视图前,dom突变前)
mutation(渲染视图中,dom突变)
layout(渲染视图后,dom突变后)
beforeMutation
beforemutation代码删除了dev环境跟一些兼容代码,剩下的主要是这些
// 在beforeMutation之前,我们还有一些准备工作
// 如清理之前存在的useEffect回调以及其他同步任务,因为这些任务可能触发新的渲染
// 所以这里要一直遍历执行 直到没有任务
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// ...
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
// ...
root.finishedWork = null;
root.finishedLanes = NoLanes;
// 对于之前提到的effectList,之前的处理逻辑只能递归到根节点rootFiber的子fiber节点
// 而rootFiber是没有加入判断的,如果workInProgress树的rootFiber有副作用
// 那么要把这个fiber加入到effectList链表中,所以下面多了这一步的判断
let firstEffect;
if (finishedWork.flags > PerformedWork) {
// A fiber's effect list consists only of its children, not itself. So if
// the root has an effect, we need to add it to the end of the list. The
// resulting list is the set that would belong to the root's parent, if it
// had one; that is, all the effects in the tree including the root.
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// There is no effect on the root.
firstEffect = finishedWork.firstEffect;
}
// 将之前的上下文保存,后面commit结束后恢复, 并将当前的上下文标记为commit阶段
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
const prevInteractions = pushInteractions(root);
// 正式进入我们的commitBeforeMutation 流程
do {
try {
commitBeforeMutationEffects();
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const current = nextEffect.alternate;
// ...
const flags = nextEffect.flags;
// 如果当前是class组件,并且有getSnapshotBeforeUpdate方法
// 则该class组件对应的fiber会有Snapshot的副作用
if ((flags & Snapshot) !== NoFlags) {
commitBeforeMutationEffectOnFiber(current, nextEffect);
}
// 如果当前fiber有 Passive 的副作用,证明存在useEffect的调用
// 以NormalSchedulerPriority的优先级调度 flushPassiveEffects的执行
// 这里的调度是在beforeMutation阶段产生的,但是实际上是异步的,会直到layout之后才执行
// 为了避免useEffect执行的副作用会阻塞浏览器的渲染
if ((flags & Passive) !== NoFlags) {
// If there are passive effects, schedule a callback to flush at
// the earliest opportunity.
if (!rootDoesHavePassiveEffects) {
// rootDoesHavePassiveEffects赋值为true之后,只会调度一次
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
接下来我们去繁从简,简述commitBeforeMutationEffects做的内容
function commitBeforeMutationLifeCycles(
current: Fiber | null,
finishedWork: Fiber,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
return;
}
case ClassComponent: {
if (finishedWork.flags & Snapshot) {
if (current !== null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const instance = finishedWork.stateNode; // 对应class的实例
// 调用了class实例上面的getSnapshotBeforeUpdate方法
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
}
return;
}
case HostRoot: {
if (supportsMutation) {
if (finishedWork.flags & Snapshot) {
const root = finishedWork.stateNode;
clearContainer(root.containerInfo);
}
}
return;
}
case HostComponent:
case HostText:
case HostPortal:
case IncompleteClassComponent:
// Nothing to do for these component types
return;
}
}
commitBeforeMutationEffectOnFiber是 commitBeforeMutationLifeCycles 方法的别名, 里面调用了getSnapshotBeforeUpdate, 用于代替React16之前的 componentWillXXX 生命周期方法。
从React16开始,钩子前增加了UNSAFE_前缀。究其原因,是因为Stack Reconciler重构为Fiber Reconciler后,render阶段的任务可能中断/重新开始,对应的组件在render阶段的生命周期钩子(即componentWillXXX)可能触发多次。这种行为和Reactv15不一致,所以标记为UNSAFE_,但在目前的Legacy模式是不会发生fiber构建中断恢复的情况,因此之前的声明周期方法还是安全的,但是后面的Concurrent模式则是不安全的。
这里是关于新版本react 的生命周期方法:
projects.wojtekmaj.pl/react-lifec…
if ((flags & Passive) !== NoFlags) {
// If there are passive effects, schedule a callback to flush at
// the earliest opportunity.
if (!rootDoesHavePassiveEffects) {
// rootDoesHavePassiveEffects赋值为true之后,只会调度一次
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
rootDoesHavePassiveEffects的赋值为true,能让我们在后面的处理阶段中,知道我们已经调度了flushPassiveEffects的执行,在layout之后,effectList要赋值给rootWithPendingPassiveEffects,以便于在 flushPassiveEffects执行中,根据Passive副作用遍历effectList,调用useEffect
在effectList上,存在带有各种副作用的fiber,从rootFiber.firstEffect开始,通过nextEffect串连在一起。
如:
对于fiber.tag 为HostComponent,如存在增删改dom,则会有Placement, Update, Deletion 副作用
对于fiber.tag为FunctionComponent, 如有useEffect,会有Passive副作用
对于fiber.tag为ClassComponent, 如有getSnapshotBeforeUpdate,会有Snapshot副作用
使用 scheduler调度器模块,以NormalSchedulerPriority的优先级调度了会执行useEffect相关逻辑的flushPassiveEffects。
注意这里是调度,是flushPassiveEffects是被异步执行的,目的为了避免副作用阻塞渲染,我们什么时候需要异步处理跟同步处理参考下面链接
zh-hans.reactjs.org/docs/hooks-…
useEffect 从被调度到最终执行经历了三个流程
- scheduler调度了flushPassiveEffect
- layout阶段之后将effectList赋值给rootWithPendingPassiveEffects
- scheduleCallback触发flushPassiveEffects,flushPassiveEffects内部遍历rootWithPendingPassiveEffects
总结,beforeMutation阶段: 处理class组件的getSnapshotBeforeUpdate生命周期方法,以NormalSchedulerPriority优先级调度了flushPassiveEffects函数
mutation
跟beforemutation阶段一样,都是遍历effectList, 这里执行的是 commitMutationEffects
nextEffect = firstEffect;
do {
try {
commitMutationEffects(root, renderPriorityLevel);
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
// commitMutationEffects 源码
function commitMutationEffects(
root: FiberRoot,
renderPriorityLevel: ReactPriorityLevel,
) {
while (nextEffect !== null) {
const flags = nextEffect.flags;
// 重置文本
if (flags & ContentReset) {
commitResetTextContent(nextEffect);
}
// 如果有使用ref,重置ref的值 为null
if (flags & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
// The following switch statement is only concerned about placement,
// updates, and deletions. To avoid needing to add a case for every possible
// bitmap value, we remove the secondary effects from the effect tag and
// switch on that value.
const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
switch (primaryFlags) {
case Placement: {
// 插入dom节点
commitPlacement(nextEffect);
nextEffect.flags &= ~Placement;
break;
}
case PlacementAndUpdate: {
// Placement 先插入dom节点
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
nextEffect.flags &= ~Placement;
// Update 再更新dom节点的属性
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// Hydrat 开头 都是 SSR相关的
case Hydrating: {
nextEffect.flags &= ~Hydrating;
break;
}
// 顾名思义,就是Hydrate 然后还有触发属性更新
case HydratingAndUpdate: {
nextEffect.flags &= ~Hydrating;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 更新dom节点
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 删除dom节点
case Deletion: {
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
commitMutationEffects按照顺序,分别执行了 重置文本节点,重置ref引用为null,执行插入/更新/删除 dom节点的操作
Placement Effect
当有placement的effect时候,会去调用commitPlacement
function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}
// Recursively insert all host nodes into the parent.
// 向上递归寻找tag为HostComponent|HostRoot|HostPortal 的父级fiber节点
// 只有这些节点才会存在dom相关的信息
const parentFiber = getHostParentFiber(finishedWork);
// Note: these two variables *must* always be updated together.
let parent;
let isContainer;
const parentStateNode = parentFiber.stateNode;
switch (parentFiber.tag) {
case HostComponent:
parent = parentStateNode;
isContainer = false;
break;
case HostRoot:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
case HostPortal:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
default:
invariant(
false,
'Invalid host parent fiber. This error is likely caused by a bug ' +
'in React. Please file an issue.',
);
}
if (parentFiber.flags & ContentReset) {
// Reset the text content of the parent before doing any insertions
resetTextContent(parent);
// Clear ContentReset from the effect tag
parentFiber.flags &= ~ContentReset;
}
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
if (isContainer) {
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
insertOrAppendPlacementNode(finishedWork, before, parent);
}
}
这个过程通过 getHostParentFiber 向上递归寻找tag为Host相关的父级fiber节点,如 HostComponent(普通html标签创建的),HostRoot(React.render创建的),HostPortal(React.createPortal创建),他们都有对应的dom节点。
通过getHostSibling获取相邻下一个的host节点, 根据before节点是否存在,最终会调用parentNode.appendChild(newNode) 或者 parentNode.insertBefore(newNode, referenceNode)
Update Effect
有update effect 副作用的fiber,会调用 commitWork
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
// Layout effects are destroyed during the mutation phase so that all
// destroy functions for all fibers are called before any create functions.
// This prevents sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
return;
}
case ClassComponent: {
return;
}
case HostComponent: {
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// Commit the work prepared earlier.
const newProps = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldProps = current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
// TODO: Type the updateQueue to be specific to host components.
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
if (updatePayload !== null) {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
}
}
return;
}
case HostText: {
const textInstance: TextInstance = finishedWork.stateNode;
const newText: string = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldText: string =
current !== null ? current.memoizedProps : newText;
commitTextUpdate(textInstance, oldText, newText);
return;
}
case HostRoot: {
if (supportsHydration) {
const root: FiberRoot = finishedWork.stateNode;
if (root.hydrate) {
// We've just hydrated. No need to hydrate again.
root.hydrate = false;
commitHydratedContainer(root.containerInfo);
}
}
return;
}
}
}
我们主要对 Function fiber 跟HostComponent fiber 来分析:
对于Function fiber,会调用commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);HookLayout表示使用了useLayoutEffect,HookHasEffect 其实是HasEffect,表示这个hooks是否应该触发。
以下是关于ReactHooksEffectTags的类型
export const NoFlags = /* */ 0b000;
// Represents whether effect should fire.
export const HasEffect = /* */ 0b001;
// Represents the phase in which the effect (not the clean-up) fires.
export const Layout = /* */ 0b010; // useLayoutEffect
export const Passive = /* */ 0b100; // useEffect
function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
遍历effectList,找出执行了useLayoutEffect 函数的销毁函数,并执行它
useLayoutEffect 分为创建函数 跟 销毁函数
useLayoutEffect(() => {
// 创建函数
return () => {
// 销毁函数
}
},[])
对于HostComponent Fiber
主要是拿到该fiber上stateNode(dom节点), 新的属性memoizedProps与 更新队列updateQueue(updateQueue我们在completeWork的时候就已经处理过了,会赋值一个数组,第i项是更新的key,第i+1项是更新的value)
然后调用commitUpdate(instance,updatePayload,type,oldProps,newProps,finishedWork)
export function commitUpdate(
domElement: Instance,
updatePayload: Array<mixed>,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
): void {
// ...
// Apply the diff to the DOM node.
updateProperties(domElement, updatePayload, type, oldProps, newProps);
}
// Apply the diff.
export function updateProperties(
domElement: Element,
updatePayload: Array<any>,
tag: string,
lastRawProps: Object,
nextRawProps: Object,
): void {
// ...
const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
// Apply the diff.
updateDOMProperties(
domElement,
updatePayload,
wasCustomComponentTag,
isCustomComponentTag,
);
// ...
}
function updateDOMProperties(
domElement: Element,
updatePayload: Array<any>,
wasCustomComponentTag: boolean,
isCustomComponentTag: boolean,
): void {
// TODO: Handle wasCustomComponentTag
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
// 更新style
if (propKey === STYLE) {
setValueForStyles(domElement, propValue);
// 更新DANGEROUSLY_SET_INNER_HTML
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
setInnerHTML(domElement, propValue);
// 设置textContent <span>1111</span> 类似这种fiber ,tag 为HostComponent, children为 1111, 不会单独为1111创建fiber
} else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else {
// 其他属性调用: node.setAttribute(attributeName, attributeValue);
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
}
Deletion effect
function commitDeletion(
finishedRoot: FiberRoot,
current: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
// Recursively delete all host nodes from the parent.
// Detach refs and call componentWillUnmount() on the whole subtree.
unmountHostComponents(finishedRoot, current, renderPriorityLevel);
const alternate = current.alternate;
detachFiberMutation(current);
if (alternate !== null) {
detachFiberMutation(alternate);
}
}
对于有Deletion Effect的 Fiber节点,
主要是 调用 unmountHostComponents 执行副作用,并且调用detachFiberMutation 重置fiber节点属性引用,释放内存。
unmountHostComponents 主要做了两件事:
- 对于HostComponent或者HostText类型的Fiber节点,找到离该 Fiber最近的父级 Host类型 Fiber(因为他们在dom层次的关系是 父子关系),调用parentNode.removeChild(node),将fiber.stateNode, 也就是dom节点从视图移除。
- 调用commitNestedUnmounts方法, 对当前fiber以及其子孙fiber递归执行commitUnmount
这些调用了commitUnmountfiber的fiber,针对不同tag类型进行处理
- 对于Function component,会将useEffect 描述节点 加入 pendingPassiveHookEffectsUnmount,之后调度flushPassiveEffects执行useEffect的销毁函数
- 对于ClassComponent,调用实例的componentWillUnmount 函数, 并且重置ref引用为null
- 对于HostComponent,重置ref引用为null
layout
该阶段在mutation节点之后,此时dom已经都渲染好了,可以开始执行一些不会阻塞浏览器的操作了,如 对于Function component调度useEffect函数,执行useLayoutEffect函数,对于 classCompnent则会执行componentDidMount/componentDidUpdate
commitMutationEffects(root, renderPriorityLevel);
// The work-in-progress tree is now the current tree. This must come after
// the mutation phase, so that the previous tree is still current during
// componentWillUnmount, but before the layout phase, so that the finished
// work is current during componentDidMount/Update.
root.current = finishedWork;
// The next phase is the layout phase, where we call effects that read
// the host tree after it's been mutated. The idiomatic use case for this is
// layout, but class component lifecycles also fire here for legacy reasons.
nextEffect = firstEffect;
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
while (nextEffect !== null) {
const flags = nextEffect.flags;
if (flags & (Update | Callback)) {
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
if (flags & Ref) {
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
- root.current = finishedWork; 前面提到的react fiber双缓存机制在这里体现出来了,由于此时workInProgress fiber 树已经渲染到视图了,那么此时要更改FiberRootNode的current指向,workinProgress Fiber树 变成了current Fiber树
- commitLayoutEffectOnFiber 其实是 commitLifeCycles的别名,针对不同tag类型,调用不同的处理方法,针对FunctionComponent(包含FunctionComponent,ForwardRef,SimpleMemoComponent)等类型,会调用commitHookEffectListMount 执行 useLayoutEffect函数
function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// Mount
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
调用 schedulePassiveEffects , 将useEffect的执行函数,跟销毁函数入栈,调度 flushPassiveEffects 的执行,最终会批量执行useEffect
function schedulePassiveEffects(finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const {next, tag} = effect;
if (
(tag & HookPassive) !== NoHookEffect &&
(tag & HookHasEffect) !== NoHookEffect
) {
enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
enqueuePendingPassiveHookEffectMount(finishedWork, effect);
}
effect = next;
} while (effect !== firstEffect);
}
}
对于class Component,会根据current fiber是否存在,调用实例的componentDidMount/componentDidUpdate(prevProps,prevState), 调用commitAttachRef更新拥有ref引用
转载自:https://juejin.cn/post/7206958960770613304