「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」
今天来的目标是分析 renderWithHooks 、bailoutHooks 是怎么实现的,这些内容不出意外就是捋顺 react 是怎么进行渲染更新的关键环节了。
renderWithHooks
突然意识到我之前似乎已经写过关于 renderWithHooks 的文章了,传送门。但是讲的也不完整,所以这里还是再补充一定内容。
export function renderWithHooks<Props, SecondArg> (
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {}
复制代码
返回类型是 any ,这不能得到什么有效信息,但是没有关系,在实现代码中我们能看到它的返回值是最后一次调用Component(props, secondArg)
的返回值。
我们给 renderWithHooks 传的 Component
参数是 workInProgress.type
,也就是用户定义的函数式组件函数。而我们分别记得在函数式组件定义中,我们返回的是用 jsx 语法定义的一些 xml 标签,这些 xml 标签会被编译成 React.createElement 函数。所以我们的 renderWithHooks
的返回值是 React.createElement
的返回值,也就是以下这种结构的东东:
const element = {
// 这个取值表示是 element
$$typeof: REACT_ELEMENT_TYPE,
// elment 的标签,比如 div, span
type: type,
key: key,
ref: ref,
props: props,
// 负责创建该 element 的组件
_owner: owner,
};
复制代码
current
参数则是 workInProgress.alternate
。
在 Component
函数中,我们可能会用到 useEffect
函数,它是怎么实现的?
对于更新阶段执行的 useEffect,执行链路是 useEffect -> updateEffect -> updateEffectImpl
,实现如下:
function updateEffectImpl (fiberFlags, hookFlags, create, deps): void {
const hook = updateWorkInProgressHook()
const nextDeps = deps === undefined ? null : deps
let destroy = undefined
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState
destroy = prevEffect.destroy
if (nextDeps !== null) {
const prevDeps = prevEffect.deps
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps)
return
}
}
}
currentlyRenderingFiber.flags |= fiberFlags
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
)
}
复制代码
其中的 pushEffect 返回的是一个 Effect 对象,结构如下:
export type Effect = {|
tag: HookFlags,
create: () => (() => void) | void,
destroy: (() => void) | void,
deps: Array<mixed> | null,
next: Effect,
|}
复制代码
其中 destroy 回调就是 create 回调的返回值。 pushEffect 的实现逻辑也很简单,如下所示:
- 创建 effect 对象
- 检查 currentlyRenderingFiber.updateQueue,如果为null,则调用 createFunctionComponentUpdateQueue 为其实例化一个 updateQueue
- updateQueue 是一个循环单链表结构,存储最后一个Effect的指针 lastEffect,将effect 对象加入到链表尾部,更新 lastEffect 的指向
此时,我们清晰认识到了 fiber 的 updateQueue 存储的是用户用 useEffect 定义的副作用代码,那么就想关注这个 updateQueue 会在什么时候被执行,也许,它跟 reconcileChildren 有关。
reconcileChildren
它的作用是更新 workInProgress.child,过程中可能着副作用。
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,
)
}
}
复制代码
reconcileChildFibers 签名如下
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
复制代码
如果 newChild 是一个 key 为 null 的 fragment 节点(参考 Fragments-React说明),则令newChild = newChild.props.children;
如果 newChild 是一个非空 object,则根据newChild.$$typeof
进行不同的操作,如果 newChild 是一个 element 节点( $$typeof = REACT_ELEMENT_TYPE
),则函数返回 placeSingleChild 调用,而 placeSingleChild 的传参是一个 reconcileSingleElement 调用。
bailoutHooks
核心实现如下:
export function bailoutHooks(
current: Fiber,
workInProgress: Fiber,
lanes: Lanes,
) {
workInProgress.updateQueue = current.updateQueue;
workInProgress.flags &= ~(PassiveEffect | UpdateEffect);
current.lanes = removeLanes(current.lanes, lanes);
}
复制代码
也就是说,该函数的作用是更新 workInProgress 的 updateQueue、清除 PassiveEffect 和 UpdateEffect标记,并从 current.lanes 中移除 lanes。
我们回忆一下,在上一层也就是 updateFunctionComponent
函数中是怎么调用这个 bailout 函数的?
首先,它是在执行完 renderWithHooks 函数之后、current 不为 null 且 didReceiveUpdate 为 false 的时候调用的,其次,它的调用形式如下:
bailoutHooks(current, workInProgress, renderLanes)
复制代码
也就是说 workInProgress 的 alternate 上的 updateQueue 会同步到 workInProgress 上的 updateQueue,相当于由于没有接受update,则将 workInProgress.updateQueue还原为旧updateQueue了
\