前端开发入门(3)-常见的React hooks

本篇我们来了解一下常见的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没有变化,返回上一次执行的结果,如果变化了,重新执行闭包并返回结果。

相关推荐
恋猫de小郭5 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅11 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606112 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了12 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅12 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅13 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅13 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment13 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅13 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊13 小时前
jwt介绍
前端