本篇我们来了解一下常见的React hooks的实现原理。包括 useState、useEfect、useCallback、useMemo。
hooks的实现都在 ReactFiberHooks.js
这个文件。
mount & update
在创建fiber节点的时候,函数组件会执行 renderWithHooks 方法,这个方法内部定义了当前的 ReactSharedInternals.H:
sql
ReactSharedInternals.H = current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate
当这个节点是null,说明这个组件对应的节点是新创建的时候,H为 HooksDispatcherOnMount,如果节点不为null,说明当前是刷新状态,H为 HooksDispatcherOnIUpdate。这两个对象都是 Dispatcher 这个类型。
这两个Dispatcher内部都定义了hooks方法,这也是我们使用hooks的时候会调用的方法,所以在关注hooks原理的时候,我们需要分别查看他们在组件mount和组件update的时候的逻辑,例如 useState:
updateWorkInProgressHook
这里提前介绍一个函数 updateWorkInProgressHook,这个函数每个hook刷新的时候都会调用到,并且逻辑比较长,所以我们先搞清楚这个函数的行为,方便我们后面理解hooks的刷新行为:
ini
function updateWorkInProgressHook(): Hook {
let nextCurrentHook: null | Hook;
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
代码的if-else比较多,我们把他变成一个流程图:
通过这个流程我们可以读懂这个函数的作用,就是在刷新的时候返回之前的hooks,只是hooks可能从workInProgressHook 复用,也可能从 currentHook 克隆。总之,React保留了之前的hooks及其状态。
useState
useState 是我们使用的最多的hooks,用来定义组件的状态,获取刷新状态的函数。
useState 在mount的时候调用的 mountState:
ini
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountStateImpl(initialState);
const queue = hook.queue;
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
null,currentlyRenderingFiber,queue
): any);
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
mountState调用链路如下图:
这里能看到 mountState 的逻辑就是创建hook对象,然后放到一个链表里面去。然后返回一个数组,数组里面是当前的初始化状态和设置状态的函数。设置状态刷新ui在React刷新的文章里面有具体描述,这里就不细看了。
接着看刷新的时候 useState 的调用,也就是 updatetState:
php
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, initialState);
}
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
这里直接调用的 updateReducer, basicStateReducer就是传一个state和一个action,并且执行这个action。
ini
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
return updateReducerImpl(hook, ((currentHook: any): Hook), reducer);
}
updateReducerImpl逻辑简化如下:
ini
function updateReducerImpl<S, A>(
hook: Hook,
current: Hook,
reducer: (S, A) => S,
): [S, Dispatch<A>] {
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
let baseQueue = hook.baseQueue;
const pendingQueue = queue.pending;
//...
const baseState = hook.baseState;
if (baseQueue === null) {
hook.memoizedState = baseState;
} else {
do {
//...
const action = update.action;
if (update.hasEagerState) {
newState = ((update.eagerState: any): S);
} else {
newState = reducer(newState, action);
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
循环内部的逻辑比较复杂,这里直接简化掉了,不影响我们理解 updateState 的流程。循环会依次处理 Update 对象,利用 reducer 把 state 更新成最新的值。最后存储在 hook.memoizeState 里面并返回。
useEffect
mount的时候调用 mountEffect -> mountEffectImpl :
javascript
function mountEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
createEffectInstance(),
nextDeps,
);
}
调用pushEffect:
ini
function pushEffect(
tag: HookFlags,
create: () => (() => void) | void,
inst: EffectInstance,
deps: Array<mixed> | null,
): Effect {
const effect: Effect = {
tag,
create,
inst,
deps,
// Circular
next: (null: any),
};
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
}
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;
}
这里看到会创建Effect对象,然后把对象插入到一个环形链表的结尾。Effect里添加了 HookHasEffect 这个tag,表示effect需要被执行。
更新的时候调用updateEffect -> updateEffectImpl:
ini
function updateEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const effect: Effect = hook.memoizedState;
const inst = effect.inst;
if (currentHook !== null) {
if (nextDeps !== null) {
const prevEffect: Effect = currentHook.memoizedState;
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);
return;
}
}
}
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
inst,
nextDeps,
);
}
这里也简单,拿到之前的hook对象,通过 areHookInputEqual 判断两次的 deps 是否一样,如果一样,只是添加Effect对象,但是不添加 HookHasEffect 这个tag,也就是不执行 effect。如果deps不一样,那么就正常添加一个需要执行的 Effect 对象。
areHookInputEuqal逻辑如下:
javascript
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
): boolean {
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;
}
新旧deps数组会按下标进行严格比较。这里需要主要的是如果你的deps是null,也就是useEffect没有传deps参数的时候,对比是直接返回false的。这就是为什么useEffect传[],闭包里的逻辑只执行一次,但是如果不传,组件每次刷新的时候,都会重新执行。
effect的执行
前篇我们了解过React会在commit阶段的 mutation 过程里调用commitHookEffectListMount和 commitHookEffectListUnMount来执行effect相关的逻辑:
ini
// commitHookEffectListMount
export function commitHookEffectListMount(
flags: HookFlags,
finishedWork: Fiber,
) {
do {
// flags包括了HookHasEffect
if ((effect.tag & flags) === flags) {
let destroy;
const create = effect.create;
const inst = effect.inst;
destroy = create();
inst.destroy = destroy;
}
effect = effect.next;
} while(effect !== firstEffect);
}
// commitHookEffectListUnMount
export function commitHookEffectListUnmount(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
do {
if ((effect.tag & flags) === flags) {
const inst = effect.inst;
const destroy = inst.destroy;
if (destroy !== undefined) {
inst.destroy = undefined;
// safelyCallDestory就是调用destroy();
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
}
}
effect = effect.next;
} while(effect !== firstEffect);
}
在mount的时候会依次执行effect链表里符合执行的Effect。unmount的时候会执行闭包返回的销毁函数。
这部分也可以解释为什么React不允许我们在条件中使用useEffect,因为effect的执行依赖effect链表的顺序,如果你把顺序破坏了,等组件unmout的时候,执行的destory可能就是错误的。
scss
const [state,setState] = useState(0);
if (state === 0) {
useEffect(()=>{
console.log('state === 0');
});
} else {
useEffect(()=>{
console.log('state !== 0');
});
}
上面这样写代码就会出现这样的错误:
useCallback
mount的时候调用mountCallback:
ini
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
)
很简单,把callback和依赖数组返回。
update的时候调用updateCallback:
ini
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
)
当deps变化了,返回新的callback。否则返回上一次的callback。deps的对比规则和useEffect一致。
useMemo
mount的时候调用 mountMemo:
ini
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
这里会返回闭包执行后的结果。
更新的时候调用 updateMemo:
ini
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
) {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
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;
}
这里同样是对比新旧deps,如果deps没有变化,返回上一次执行的结果,如果变化了,重新执行闭包并返回结果。