开篇
上一节中我们讲到了两个标准的State Hook
(状态Hook),而这一节我们就去详细分析一下Effect Hook
(副作用hook)
Effect Hook
前面我们已经对状态hook
有了一定的了解,而对于effect hook
最标准的就是useEffect
和useLayoutEffect
。
1.useEffect和useLayoutEffect
我们先简单分析useEffect
和useLayoutEffect
的入参,他们基本是相同的唯一的不同仅有hook
标示符HookPassive
和HookLayout
,最后都是调用mountEffectImpl
。
create
:传入useEffect
函数,即回调函数。
deps
:依赖项,用来控制该Effect包裹的函数执不执行。如果依赖项为空数组[]
,则该Effect在每次组件挂载时执行,且仅执行一次,没有第二个参数则一直执行。
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps,
);
}
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
UpdateEffect,
HookLayout,
create,
deps,
);
}
// Represents the phase in which the effect (not the clean-up) fires.
export const Layout = /* */ 0b010;
export const Passive = /* */ 0b100;
关于创建hook
我们前面有提到,状态hook.memoizedState
绑定的是状态,而副作用hook.memoizedState
绑定的则是一个effect对象。这里去确立fiber
的flag
是为了后续渲染fiber树
使用的。
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 创建hook,确定在fiber.memoizedState链表中的位置
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 确定fiber的flag
currentlyRenderingFiber.flags |= fiberFlags;
// 挂载在hook.memoizedState上,state hook中挂载的是state状态,而effect hook中挂载的是effect链表
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}
在这个effect hook
中,我们以一个hook
的视角讲一下指向,hook.memoizedState
指向effct
,effct
是一个环形链表,他同时被挂载在fiber.updateQueue.lastEffect
和hook.memoizedState
上。这里的逻辑也很简单就是处理创建effct
,指向effct
到链尾,返回effct
。
function pushEffect(tag, create, destroy, deps) {
// 创建effect节点
const effect: Effect = {
tag,
create,
destroy,
deps,
next: (null: any),
};
// 把effect节点指向链尾
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
// 新建 currentlyRenderingFiber.updateQueue = workInProgress.updateQueue 用于挂载effect对象
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
// updateQueue.lastEffect是一个环形链表
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
// 返回effect
return effect;
}
紧接着我们就应该是去处理Effect
的回调了,而Effect
的回调是在commitRootImpl
中的提交阶段
去执行的,我们回顾一下一下提交阶段的三个子阶段:dom
更新前commitBeforeMutationEffects、dom
更新 commitMutationEffects、dom
变更后 commitLayoutEffects。fiber
的提交我们可以回顾一下前文,我们这里只关注对hook
的处理。
1.1.dom更新之前
提交子阶段1中有提到,在这我们以一个effct hook
视角来观察这个过程,在这里我们只关心一件事情,就是在变更之前将flushPassiveEffects
交给了调度中心,去异步调用,而我们的整个commit
在17.2中是同步的,而flushPassiveEffects
是处理useEffect
的,我们的后面几个阶段会为flushPassiveEffects
处理useEffect
先做一些准备工作,当准备工作做完后,我们再来分析flushPassiveEffects
。
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
if ((flags & Passive) !== NoFlags) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
}
}
1.2.dom变更
在前文中我们提到了,如果需要更新节点,会执行commitWork
,运算后useEffect
,useLayoutEffect
的值为Update
,进去Update
将内存中的fiber
用于提交操作
function commitMutationEffects(
root: FiberRoot,
renderPriorityLevel: ReactPriorityLevel,
) {
while (nextEffect !== null) {
const flags = nextEffect.flags;
// 运算后实际useEffect,useLayoutEffect的值为Update
const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
switch (primaryFlags) {
case Update: {
// 将内存的中的fiber指向当前,进行更新节点操作
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
commitWork
在dom
变更阶段对于effect hook
会处理销毁处理上一次的effectLayout
来保证这一次阶段dom
变更之后effectLayout
的执行。
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
// 销毁effectLayout函数
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
}
return;
}
case ClassComponent: {
return;
}
}
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 {
// 这里只销毁effectLayout,对比的时候effct.tag如果为0不变更即不执行
if ((effect.tag & tag) === tag) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
1.3 变更后
commitLayoutEffects
这里我们在前面的构建章已经讲过,我们回顾一下,在这里的schedulePassiveEffects
,为我们flushPassiveEffects
提供了能够脱离fiber节点
直接去访问effects
的条件,因为它为我们保存了两个全局销毁挂载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;
// 筛选出由useEffect()创建的effect
if (
(tag & HookPassive) !== NoHookEffect &&
(tag & HookHasEffect) !== NoHookEffect
) {
// 把effect添加到全局数组, 等待flushPassiveEffectsImpl处理
enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
enqueuePendingPassiveHookEffectMount(finishedWork, effect);
}
effect = next;
} while (effect !== firstEffect);
}
}
此时我们的准备工作已经做好了,来到处理useEffect
的阶段。
export function flushPassiveEffects(): boolean {
// Returns whether passive effects were flushed.
if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) {
const priorityLevel =
pendingPassiveEffectsRenderPriority > NormalSchedulerPriority
? NormalSchedulerPriority
: pendingPassiveEffectsRenderPriority;
pendingPassiveEffectsRenderPriority = NoSchedulerPriority;
// `runWithPriority`设置Schedule中的调度优先级, 如果在flushPassiveEffectsImpl中处理effect时又发起了新的更新, 那么新的update.lane将会受到这个priorityLevel影响.
return runWithPriority(priorityLevel, flushPassiveEffectsImpl);
}
return false;
}
function flushPassiveEffectsImpl() {
if (rootWithPendingPassiveEffects === null) {
return false;
}
rootWithPendingPassiveEffects = null;
pendingPassiveEffectsLanes = NoLanes;
// 执行所有的useEffect销毁函数,调用useEffect在上一次的渲染的销毁函数
const unmountEffects = pendingPassiveHookEffectsUnmount;
pendingPassiveHookEffectsUnmount = [];
for (let i = 0; i < unmountEffects.length; i += 2) {
const effect = ((unmountEffects[i]: any): HookEffect);
const fiber = ((unmountEffects[i + 1]: any): Fiber);
const destroy = effect.destroy;
effect.destroy = undefined;
if (typeof destroy === 'function') {
destroy();
}
}
// 执行所有的useEffect回调函数, 调用useEffect这次render的回调函数,重新赋值到 effect.destroy
const mountEffects = pendingPassiveHookEffectsMount;
pendingPassiveHookEffectsMount = [];
for (let i = 0; i < mountEffects.length; i += 2) {
const effect = ((mountEffects[i]: any): HookEffect);
const fiber = ((mountEffects[i + 1]: any): Fiber);
effect.destroy = create();
}
}
到这我们分析mount Effct
的过程已经告于段落。
1.4 更新effect
这里的逻辑和前面挂载effect几乎一样,多了最主要的内容是依赖比较(浅比较),如果依赖相同传hookFlags
保证顺序,如果不同传HookHasEffect | hookFlags
,HookHasEffect
表示commit阶段需要重新执行。
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;
// 继续使用先前effect.destroy
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}
// 浅比较
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
if (prevDeps === null) {
return false;
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
然后剩下的就和前面逻辑保持一致,commit
会判断effect.tag
决定是否执行。
ref hook
这里ref hook
确实比较简单,相信如果有能力读懂上面写的,几乎ref
是一看就懂的
export function useRef<T>(initialValue: T): {|current: T|} {
const dispatcher = resolveDispatcher();
return dispatcher.useRef(initialValue);
}
function mountRef<T>(initialValue: T): {|current: T|} {
const hook = mountWorkInProgressHook();//获取useRef
const ref = {current: initialValue};//ref初始化
hook.memoizedState = ref;
return ref;
}
// update时调用updateRef获取获取当前useRef,然后返回hook链表
function updateRef<T>(initialValue: T): {|current: T|} {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
构建fiber
阶段探寻,回溯处理
//beginWork中
function markRef(current: Fiber | null, workInProgress: Fiber) {
const ref = workInProgress.ref;
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
workInProgress.effectTag |= Ref;
}
}
//completeWork中
function markRef(workInProgress: Fiber) {
workInProgress.effectTag |= Ref;
}
提交阶段二,子阶段二commitMutationEffects
dom变更,如果ref改变了,先删除.
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// ...
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);//移除ref
}
}
}
提交阶段二,子阶段三commitLayoutEffect
,重新设置ref
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) {
// 重新设置ref
commitAttachRef(nextEffect);
}
// 忽略代码
nextEffect = nextEffect.nextEffect;
}
// 忽略代码
}
useMemo和useCallback
mount
挂载
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();//创建hook
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();//计算value
hook.memoizedState = [nextValue, nextDeps];//把value和依赖保存在memoizedState中
return nextValue;
}
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();//创建hook
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];//把callback和依赖保存在memoizedState中
return callback;
}
update
更新
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();//获取hook
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {//浅比较依赖
return prevState[0];//没变 返回之前的状态
}
}
}
const nextValue = nextCreate();//有变化重新调用callback
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();//获取hook
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {//浅比较依赖
return prevState[0];//没变 返回之前的状态
}
}
}
hook.memoizedState = [callback, nextDeps];//变了重新将[callback, nextDeps]赋值给memoizedState
return callback;
}
总结
hook
这一块这几天,感觉还是不够深入,这段时间先打打断点,深入分析一下,调用时机,算法,再重新修一下这整个大章节。