前端开发入门(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没有变化,返回上一次执行的结果,如果变化了,重新执行闭包并返回结果。

相关推荐
王解6 分钟前
Vue CLI 脚手架创建项目流程详解 (2)
前端·javascript·vue.js
刘大浪10 分钟前
vue.js滑动到顶便锁定位置
前端·javascript·vue.js
小金刚®16 分钟前
构建简洁之美:我的第一个前端页面
前端
ordinary901 小时前
指令-v-for的key
前端·javascript·vue.js
rpa_top1 小时前
RPA 助力电商:自动化商品信息上传,节省人力资源 —— 以影刀 RPA 为例【rpa.top】
大数据·前端·人工智能·自动化·rpa
新时代农民工--小明1 小时前
前端自动化部署更新,自动化打包部署
运维·前端·自动化
前端Hardy1 小时前
HTML&CSS:酷炫的3D开关控件
前端·javascript·css·3d·html
小马超会养兔子1 小时前
如何写一个数字老虎机滚轮
开发语言·前端·javascript·vue
m0_672449602 小时前
前端(组件传参案例)
前端
devotemyself2 小时前
vue的ElMessage的css样式不生效
前端·css·vue.js