接上一篇,这一篇再说一下另外几个常用的 hook
:useEffect
、useLayout
、useRef
、useMemo
、useCallback
、useImperativeHandle
。
useEffect 与 useLayoutEffect
useEffect
与 useLayoutEffect
除了在执行时机上不一样其它基本上都一样。本文将以 useEffect
为主去介绍两个 API
。
useEffect
与 useLayoutEffect
的生成也分为 mount
和 update
阶段。
mount
mount
阶段主要是进行副作用的挂载,关键的函数调用是 mountEffect(mountLayoutEffect)
-> mountEffectImpl
-> pushEffect
,pushEffect
主要作用就是把副作用挂到 fiber
的 updateQueue
属性上,与 useState
的 update
对象一致,也是一个环状链表结构。
// path: packages/react-reconciler/src/ReactFiberHooks.new.js
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= fiberFlags;
// 副作用挂到 memoizedState 的时候也被挂到了 updateQueue 上
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy,
deps,
// Circular
next: (null: any),
};
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
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;
}
}
return effect;
}
生成的副作用的数据结构如下图所示:
update
update
阶段和 mount
阶段做的时候大致相同,但是增加了对依赖项的判断。其关键调用的函数 updateEffect(updateLayoutEffect)
-> updateEffectImpl
-> pushEffect
。
// path: packages/react-reconciler/src/ReactFiberHooks.new.js
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,
);
}
上面这段代码中 updateEffectImpl
函数里面会判断依赖项(deps) 其后是否有变化,调用了 areHookInputsEqual
函数,这个函数内部实际上是用了 Object.is
这个 API
来判断的。
调用过程
useEffect
与 useLayoutEffect
的调用过程有些不同,在 React 源码阅读 - 渲染 中提到了,useEffect
在 commit
阶段的 before mutation
阶段进入之前会调用 flushPassiveEffects
函数。而 useLayoutEffect
则是在 commit
阶段的 mutation
和 layout
阶段调用的。
不过最后会经过调度器的处理,所以 useEffect
最终表现出来执行时机比 uesLayoutEffect
要晚。
useEffect
flushPassiveEffects
内部基本都是一些设置优先级的操作,然后调用了 flushPassiveEffectsImpl
函数,从而触发 useEffect
的调用。
// path: packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function flushPassiveEffectsImpl() {
// 省略一些代码
// 调用上一次 useEffect 的销毁函数
commitPassiveUnmountEffects(root.current);
// 调用本次 useEffect 的回调函数
commitPassiveMountEffects(root, root.current);
// 省略一些代码
flushSyncCallbacks();
// 省略一些代码
return true;
}
销毁函数执行
销毁函数也就是我们在 useEffect
的回调中写的 return
的那部分函数。
以官网的一段代码为例:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
ChatAPI.unsubscribeFromFriendStatus
就是销毁函数部分。return
以外的就是回调函数部分。
commitPassiveUnmountEffects
函数正是执行销毁函数的地方,它内部又调用了 commitPassiveUnmountEffects_begin
-> commitPassiveUnmountEffects_complete
-> commitPassiveUnmountOnFiber
-> commitHookEffectListUnmount
。
// path: packages/react-reconciler/src/ReactFiberCommitWork.new.js
function commitHookEffectListUnmount(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
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 & flags) === flags) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
回调函数执行
commitPassiveMountEffects
函数正是执行回调函数的地方,它内部又调用了 commitPassiveMountEffects_begin
-> commitPassiveMountEffects_complete
-> commitPassiveMountOnFiber
-> commitHookEffectListMount
。这个过程和销毁阶段执行的过程一样。
// path: packages/react-reconciler/src/ReactFiberCommitWork.new.js
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();
if (__DEV__) {
const destroy = effect.destroy;
if (destroy !== undefined && typeof destroy !== 'function') {
let addendum;
if (destroy === null) {
addendum =
' You returned null. If your effect does not require clean ' +
'up, return undefined (or nothing).';
} else if (typeof destroy.then === 'function') {
addendum =
'\n\nIt looks like you wrote useEffect(async () => ...) or returned a Promise. ' +
'Instead, write the async function inside your effect ' +
'and call it immediately:\n\n' +
'useEffect(() => {\n' +
' async function fetchData() {\n' +
' // You can await here\n' +
' const response = await MyAPI.getData(someId);\n' +
' // ...\n' +
' }\n' +
' fetchData();\n' +
`}, [someId]); // Or [] if effect doesn't need props or state\n\n` +
'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching';
} else {
addendum = ' You returned: ' + destroy;
}
console.error(
'An effect function must not return anything besides a function, ' +
'which is used for clean-up.%s',
addendum,
);
}
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
useLayoutEffect
销毁函数执行
useLayoutEffect
销毁函数与 useEffect
的销毁函数一样,也是回调中写的 return
的那部分函数。
在 commit
阶段的 mutation
阶段,删除组件的时候会调用 commitDeletion
-> commitNestedUnmounts
-> commitUnmount
,
// 路径:packages/react-reconciler/src/ReactFiberCommitWork.new.js
function commitUnmount(
finishedRoot: FiberRoot,
current: Fiber,
nearestMountedAncestor: Fiber,
): void {
onCommitUnmount(current);
switch (current.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
if (updateQueue !== null) {
const lastEffect = updateQueue.lastEffect;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const {destroy, tag} = effect;
if (destroy !== undefined) {
if ((tag & HookLayout) !== NoHookEffect) {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
current.mode & ProfileMode
) {
startLayoutEffectTimer();
safelyCallDestroy(current, nearestMountedAncestor, destroy);
recordLayoutEffectDuration(current);
} else {
safelyCallDestroy(current, nearestMountedAncestor, destroy);
}
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
return;
}
// 省略一些代码
}
}
回调函数执行
useLayoutEffect
的回调函数调用是在 commit
阶段的 layout
阶段进行调用的。
// 路径:packages/react-reconciler/src/ReactFiberCommitWork.new.js
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
if ((finishedWork.flags & LayoutMask) !== NoFlags) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
if (
!enableSuspenseLayoutEffectSemantics ||
!offscreenSubtreeWasHidden
) {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
// 执行 useLayoutEffect 的回调函数
commitHookEffectListMount(
HookLayout | HookHasEffect,
finishedWork,
);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
}
}
break;
}
// 省略一些代码
}
}
// 省略一些代码
}
可以看到 useLayoutEffect
与 useEffect
回调的时候都一样调用了 commitHookEffectListMount
函数。
useMemo 与 useCallback
这两个 hook
比较类似,所以放一起看,这两个 hook
也分为两种情况:mount
与 update
。
mount
// path: packages/react-reconciler/src/ReactFiberHooks.new.js
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
// 创建并返回当前 hook
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 计算要返回的值
const nextValue = nextCreate();
// 将计算好的值和依赖数组保存在 hook.memoizedState
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// 创建并返回当前 hook
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 将回调函数和依赖数组保存在 hook.memoizedState
hook.memoizedState = [callback, nextDeps];
return callback;
}
可以看到,这两个挂载函数唯一的区别是:
mountMemo
会将回调函数(nextCreate)的执行结果作为value
保存mountCallback
会保存回调函数
update
// path: packages/react-reconciler/src/ReactFiberHooks.new.js
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
// 返回当前 hook
const hook = updateWorkInProgressHook();
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();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// 返回当前 hook
const hook = updateWorkInProgressHook();
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];
return callback;
}
可以看到,这两个更新函数的区别也是在于返回的是计算好的值还是回调函数。
useRef
useRef
也分为 mount
与 update
两个场景。
mount
// path: packages/react-reconciler/src/ReactFiberHooks.new.js
function mountRef<T>(initialValue: T): {|current: T|} {
// 获取当前的 hook
const hook = mountWorkInProgressHook();
if (enableUseRefAccessWarning) { // false
if (__DEV__) {
// 省略一些代码
} else {
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
}
} else {
// 创建 hook
const ref = {current: initialValue};
// 将 ref 保存在 memoizedState 属性上
hook.memoizedState = ref;
// 返回 ref
return ref;
}
}
可见,useRef
仅仅是返回一个包含 current
属性的对象。
React.createRef
做的事情和上面代码做的事情一致:
// path: packages/react/src/ReactCreateRef.js
export function createRef(): RefObject {
const refObject = {
current: null,
};
// 省略一些代码
return refObject;
}
update
// path: packages/react-reconciler/src/ReactFiberHooks.new.js
function updateRef<T>(initialValue: T): {|current: T|} {
// 获取当前 hook
const hook = updateWorkInProgressHook();
// 返回之前存储的值
return hook.memoizedState;
}
可见 useRef
如果只是用来存一些值,那么只要不主动去更新他,那么在函数组件更新的时候 useRef
是不会变的。
ref 上挂 DOM 对象的流程
在 React
中,HostComponent
、ClassComponent
、ForwardRef
可以赋值 ref
属性,如下所示。
// HostComponent
<div ref={domRef}></div>
// ClassComponent / Forward
<App ref={componentRef} />
ForwardRef
只是将 ref
作为第二参数传递下去:
const FancyInput = forwardRef((props, ref) => { // props 就算没有使用也不能省略,否则会报错
const inputRef = useRef()
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
}
}))
return (
<div>
<input ref={inputRef} type='text' />
<button onClick={() => inputRef.current.focus()}>click me!</button>
</div>
)
})
ForwardRef
组件第二参数 ref
会被传递下去,不会进入 DOM 对象挂上 ref
的流程,所以下面不讨论 Forward
的 ref
。
// path: packages/react-reconciler/src/ReactFiberHooks.new.js
// renderWithHooks 函数
let children = Component(props, secondArg);
render 阶段
首先是为含有 ref
的 fiber
添加 Ref flags
。
HostComponent: beginWork
-> updateHostComponent
-> markRef
ClassComponent: beginWork
-> updateClassComponent
-> finishClassComponent
-> markRef
// path: packages/react-reconciler/src/ReactFiberBeginWork.new.js
function markRef(current: Fiber | null, workInProgress: Fiber) {
const ref = workInProgress.ref;
if (
// mount 时 存在 ref 属性
(current === null && ref !== null) ||
// update 时 ref 属性改变
(current !== null && current.ref !== ref)
) {
// Schedule a Ref effect
workInProgress.flags |= Ref;
if (enableSuspenseLayoutEffectSemantics) {
workInProgress.flags |= RefStatic;
}
}
}
completeWork
阶段其实也有对 markRef
的调用(不是同一个 markRef
),总体来说实现是一致的,不在此赘述了。
总结起来作用就是给组件对应 fiber
赋值 Ref flag
,打上标签,以供后续阶使用。
commit 阶段
在 commit
阶段的 mutation
阶段,会对 ref
进行更改,具体调用如下:
// path: packages/react-reconciler/src/ReactFiberCommitWork.new.js
function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
const flags = finishedWork.flags;
if (flags & ContentReset) {
commitResetTextContent(finishedWork);
}
if (flags & Ref) {
const current = finishedWork.alternate;
if (current !== null) {
// 移除之前的 ref
commitDetachRef(current);
}
// 省略一些代码
}
// 省略一些代码
}
function commitDetachRef(current: Fiber) {
const currentRef = current.ref;
if (currentRef !== null) {
if (typeof currentRef === 'function') {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
current.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
// function 类型的 ref 会被调用,入参 null
currentRef(null);
} finally {
recordLayoutEffectDuration(current);
}
} else {
currentRef(null);
}
} else {
// 对象类型的 ref,current 赋值为 null
currentRef.current = null;
}
}
}
可以看出来上面这个过程是对原有 ref
进行了一个删除操作,接下来会进入 ref
的赋值阶段,这个过程是在 commit
阶段的 layout
阶段进行的。
// path: packages/react-reconciler/src/ReactFiberCommitWork.new.js
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
// 省略一些代码
if (enableScopeAPI) {
// TODO: This is a temporary solution that allowed us to transition away
// from React Flare on www.
if (finishedWork.flags & Ref && finishedWork.tag !== ScopeComponent) {
commitAttachRef(finishedWork);
}
} else {
if (finishedWork.flags & Ref) {
commitAttachRef(finishedWork);
}
}
}
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
// stateNode 存的就是 fiber 对应的 DOM 信息
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
if (enableScopeAPI && finishedWork.tag === ScopeComponent) {
instanceToUse = instance;
}
if (typeof ref === 'function') {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
// ref 赋值
ref(instanceToUse);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
ref(instanceToUse);
}
} else {
// 省略一些代码
ref.current = instanceToUse;
}
}
}
至此,ref
挂载完毕。
useImperativeHandle
useImperativeHandle
与 ref
和 Froward
配合使用,一般用于暴露某个组件的 API
供其他组件进行调用。
useImperativeHandle
的实现比较简单,直接上代码:
// path: packages/react-reconciler/src/ReactFiberHooks.new.js
function imperativeHandleEffect<T>(
create: () => T,
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
) {
if (typeof ref === 'function') {
const refCallback = ref;
const inst = create();
refCallback(inst);
return () => {
refCallback(null);
};
} else if (ref !== null && ref !== undefined) {
const refObject = ref;
// 省略一些代码
const inst = create();
refObject.current = inst;
return () => {
refObject.current = null;
};
}
}
function mountImperativeHandle<T>(
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null,
): void {
// 省略一些代码
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
let fiberFlags: Flags = UpdateEffect;
if (enableSuspenseLayoutEffectSemantics) {
fiberFlags |= LayoutStaticEffect;
}
// 省略一些代码
return mountEffectImpl(
fiberFlags,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps,
);
}
function updateImperativeHandle<T>(
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null,
): void {
// 省略一些代码
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
return updateEffectImpl(
UpdateEffect,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps,
);
}
可以看出 useImperativeHandle
就是调了一下 useEffect
的实现 mountEffectImpl
,只是第三参数有一些区别。第三参数使用了 imperativeHandleEffect
函数。
imperativeHandleEffect
函数做的事情就是:
- 执行
create
函数,得到实例; - 把实例挂到
ref 的 current
上;