React Hook再探

936 阅读9分钟

与实现原理有关的问题

固定的 Hook 调用顺序

  • 一个 React 组件,在生命周期内多次执行渲染函数时,如何知道内存中的哪个值对应代码中的哪个useState?
    • 答案是通过顺序,useSate 被执行时,如果是更新的执行则将 state 的 name 绑定在内存中第一个state值中并忽略初始化参数。
  • 所以,你必须保证无论什么情况下使用了 Hook 的 React 函数被调用内部的 Hook 执行顺序都一致
    • 这要求不可以在循环、条件或嵌套中使用 Hook
    • 更建议的是只在 React 函数的顶层调用Hook,React如何检验是否是顶层调用。 为什么要根据顺序来对应,如何记录并对应的?

set 是异步的,更新需要使用函数

  • setState( value+1 );
    • 同步多次调用只会生效一次
  • setState(value => value +1);
    • 同步多次调用结果符合预期 他是异步的,为什么异步?用异步来做什么?

Hook 是闭包实现的,储存旧的值

  • useEffect(() => setInterval(()=> log(a),[]);
    • 即使循环打印的 a 值变了也打印初始的 a,因为打印的值是保存的闭包 Hook使用闭包,为什么,怎么做的?

原理

事前明确的几个概念

  • current fiber 树:完成渲染后生成,它会在 commit 阶段替换成真实的 dom 树。
  • workInProgrss fiber树:即将调和渲染的树,在组件的更新中,从 current 复制一份作为 workInProgress,更新完毕后再将其复制回 current 树。
  • workInProgress.memoizedState:在 class 组件中,memoizedState 存放 state 信息,在 function 组件中,他在首次渲染过程中使用链表存放 hook 信息。
  • workInProgress.expirationTime:React 用不同的 expirationTime 来确定更新的优先级。
  • currentHookcurrent 树上指向当前调度的 hooks 节点。
  • worinInProgressHookworkInProgress 树上指向当前调度的 hooks 节点。

renderWithHooks

它是调用function组件函数的函数

  • A:置空 workInProgress 树的 memoizedStateupdateQueue,初始化 expirationTime
  • B:赋予 ReactCurrentDispatcher.current 不同的 hooks
    • 如果是首次渲染,则使用 HooksDispatcherOnMount hooks 对象。
    • 如果是更新渲染,则使用 HooksDIspatcherOnUpdate hooks 对象。
    • 通过 currentcurrent 上的 memoizedState 是否存在来判断是否为首次渲染。
  • C:调用 Component(props, secondArg); 执行函数组件,把 hook 依次保存到 workInProgress 上。
  • D:将 ContextOnlyDispatcher 赋值给 ReactCurrentDispatcher.current
  • E:将一些变量赋值为空,返回调用函数组件生成的结果。

附上源码

export function renderWithHooks(
  current,
  workInProgress,
  Component,
  props,
  secondArg,
  nextRenderExpirationTime,
) {
  renderExpirationTime = nextRenderExpirationTime;
  currentlyRenderingFiber = workInProgress;

  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.expirationTime = NoWork;

  ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;

  let children = Component(props, secondArg);

  if (workInProgress.expirationTime === renderExpirationTime) { 
       // ....这里的逻辑我们先放一放
  }

  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  renderExpirationTime = NoWork;
  currentlyRenderingFiber = null;

  currentHook = null
  workInProgressHook = null;

  didScheduleRenderPhaseUpdate = false;

  return children;
}

步骤D

  • 在调用函数组件后,发生了这次赋值,也就是在正确的调用一次 Hook 后,Hook 就会被替换为 ContextOnlyDispatcher,它的源码如下。也就是如果嵌套了 Hook,那么会在第二次调用时报错。
    • 异步的,更改 Hook 为报错是第一次调用的同步任务,第二次调用是新的宏任务。
const ContextOnlyDispatcher = {
    useState:throwInvalidHookError
}
function throwInvalidHookError() {
  invariant(
    false,
    'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
      ' one of the following reasons:\n' +
      '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
      '2. You might be breaking the Rules of Hooks\n' +
      '3. You might have more than one copy of React in the same app\n' +
      'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
  );
}

步骤B

是否是首次渲染,Hook 表是完全不同的两套。

Hooks 初始化

也就是初次渲染时调用 Hook,每次执行都会调用 mountWorkInProgressHook 以下为这个函数的源码。

function mountWorkInProgressHook() {
  const hook: Hook = {
    memoizedState: null,  // useState中 保存 state信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和deps | useRef中保存的是ref 对象
    baseState: null, //最新的 state 值
    baseQueue: null, //最新的更新队列
    queue: null, //保存待更新的队列
    next: null, //链表
  };
  if (workInProgressHook === null) { // 例子中的第一个`hooks`-> useState(0) 走的就是这样。
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
  • 创建一个 Hook 对象,其中有 next 属性指向下一个形成链表的形式。
  • 将新的链表头 hook 赋值给函数组件用来存放链表的 memoizedState
  • hook 对象中并没有标识属性,hook 的对应通过链表顺序来对应。 有两个memoizedState别混淆了
  • workInProgress / current 树上的 memoizedState 保存的是当前函数组件每个hooks形成的链表。
  • 每个hooks上的memoizedState 保存了当前hooks信息,不同种类的hooksmemoizedState内容不同。

mountState

function mountState(
  initialState
){
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // 如果 useState 第一个参数为函数,执行函数得到state
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    pending: null,  // 带更新的
    dispatch: null, // 负责更新函数
    lastRenderedReducer: basicStateReducer, //用于得到最新的 state ,
    lastRenderedState: initialState, // 最后一次得到的 state
  });

  const dispatch = (queue.dispatch = (dispatchAction.bind( // 负责更新的函数
    null,
    currentlyRenderingFiber,
    queue,
  )))
  return [hook.memoizedState, dispatch];
}
  • 得到初始化值,赋值给上文的 mountWorkInProgressHook
    • 并产生前俩参数值。
    • 然后创建 queue,保存负责更新的信息
  • 定义 set 函数,调用 dispatchAction 来更新 state, 返回这个定义。 dispatchAction
function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {

  // 计算 expirationTime 过程略过。
  /* 创建一个update */
  const update= {
    expirationTime,
    suspenseConfig,
    action,
    eagerReducer: null,
    eagerState: null,
    next: null,
  }
  /* 把创建的update */
  const pending = queue.pending;
  if (pending === null) {  // 证明第一次更新
    update.next = update;
  } else { // 不是第一次更新
    update.next = pending.next;
    pending.next = update;
  }
  
  queue.pending = update;
  const alternate = fiber.alternate;
  /* 判断当前是否在渲染阶段 */
  if ( fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber)) {
    didScheduleRenderPhaseUpdate = true;
    update.expirationTime = renderExpirationTime;
    currentlyRenderingFiber.expirationTime = renderExpirationTime;
  } else { /* 当前函数组件对应fiber没有处于调和渲染阶段 ,那么获取最新state , 执行更新 */
    if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        try {
          const currentState = queue.lastRenderedState; /* 上一次的state */
          const eagerState = lastRenderedReducer(currentState, action); /**/
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) { 
            return
          }
        } 
      }
    }
    scheduleUpdateOnFiber(fiber, expirationTime);
  }
}

有三个参数,但是前两个已经被 bind 改掉,我们传入的也就是要更新的新值为第三个参数 action。

  • 创建 update 对象,记录了此次更新的信息,并把这个 update 接入其他更新对象链表。
  • 将 pending 待更新对象链表指向最新的 update 对象。
  • 判断是否在渲染阶段
    • 如果不在渲染,则获取最新 state 与上一次的 currentState 浅比较
      • 如果相等则直接退出。
      • 不相等则调度渲染当前 fiber,并挂载到真实dom
    • 如果正在渲染需要排队,则更新当前 updateexpirationTime 即可。

mountEffect

function mountEffect(
  create,
  deps,
) {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag, 
    create, // useEffect 第一次参数,就是副作用函数
    undefined,
    nextDeps, // useEffect 第二次参数,deps
  );
}
  • 前面部分都大同小异
  • 最后调用了 pushEffect
function pushEffect(tag, create, destroy, deps) {
  const effect = {
    tag,
    create,
    destroy,
    deps,
    next: null,
  };
  let componentUpdateQueue = currentlyRenderingFiber.updateQueue
  if (componentUpdateQueue === null) { // 如果是第一个 useEffect
    componentUpdateQueue = {  lastEffect: null  }
    currentlyRenderingFiber.updateQueue = componentUpdateQueue
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {  // 存在多个effect
    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;
}

创建对象,判断是否是第一次创建如果是就初始化创建更新 queue,也就是就是workInProgressupdateQueue,然后把这个 effect 放入 更新队列中

mounted 阶段 Hook 小结

  • 每个 Hook 执行会生成一个 hook 对象,形成链表结构绑定在 workInProgressmemoizedState 的属性上。
  • 每个 hook 自己的档期状态绑定在 hook 对象的 memoizedState 属性上。
  • 对于 effect 钩子,绑定在 workInProgress.updateQueue 上,在 commit 时,dom树构建完毕后执行 effect 链表的每个钩子。

hooks 更新阶段

  • 从 current 的 hooks 中找到与当前 workInProgressHook 对应的 currentHooks
  • 拷贝一份给 workInProgressHook,在这份拷贝上执行更新来确保 hooks 状态不会异常丢失 执行这个过程的函数名叫**updateWorkInProgressHook**

updateWorkInProgressHook

function updateWorkInProgressHook() {
  let nextCurrentHook;
  if (currentHook === null) {  /* 如果 currentHook = null 证明它是第一个hooks */
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else { /* 不是第一个hooks,那么指向下一个 hooks */
    nextCurrentHook = currentHook.next;
  }
  let nextWorkInProgressHook
  if (workInProgressHook === null) {  //第一次执行hooks
    // 这里应该注意一下,当函数组件更新也是调用 renderWithHooks ,memoizedState属性是置空的
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else { 
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) { 
      /* 这个情况说明 renderWithHooks 执行 过程发生多次函数组件的执行 ,我们暂时先不考虑 */
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
    invariant(
      nextCurrentHook !== null,
      'Rendered more hooks than during the previous render.',
    );
    currentHook = nextCurrentHook;
    const newHook = { //创建一个新的hook
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null,
    };
    if (workInProgressHook === null) { // 如果是第一个hooks
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else { // 重新更新 hook
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

  • 如果是第一次执行 Hook,则从 cruuent 树上取出 memoizedState,也就是 hooks 链表。
  • 声明变量 nextWorkInProgressHook,回忆每次 setState 后,下一轮执行 renderWithHooks 后这个值就会被置空,只有当一次 renderWithHooks 执行中,又多次执行了函数组件。
    • 也就是上次对 renderWithHooks 解释中暂时掠过的代码,就是如果在函数组件执行后当前的更新优先级仍然是最高说明这个组建有了新的更新,那么就循环执行当前函数,此时这个值就不为空。
  • 赋值 current 的 hooks,把它赋值给 workInProgressHook。

updateSatte

function updateReducer(
  reducer,
  initialArg,
  init,
){
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  const current = currentHook;
  let baseQueue = current.baseQueue;
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
     // 这里省略... 第一步:将 pending  queue 合并到 basequeue
  }
  if (baseQueue !== null) {
    const first = baseQueue.next;
    let newState = current.baseState;
    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;
    do {
      const updateExpirationTime = update.expirationTime;
      if (updateExpirationTime < renderExpirationTime) { //优先级不足
        const clone  = {
          expirationTime: update.expirationTime,
          ...
        };
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
      } else {  //此更新确实具有足够的优先级。
        if (newBaseQueueLast !== null) {
          const clone= {
            expirationTime: Sync, 
             ...
          };
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        /* 得到新的 state */
        newState = reducer(newState, action);
      }
      update = update.next;
    } while (update !== null && update !== first);
    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = newBaseQueueFirst;
    }
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
  }
  const dispatch = queue.dispatch
  return [hook.memoizedState, dispatch];
}
  • 将上一次更新的 pending queue 合并到 basequeue。
  • 将当前的 hook 上的 baseState 和 baseQueue 更新到最新状态。
    • 循环 basequeue 中的 update,不断更新 expirationTime
    • 对于优先级达到标准的那些 update,获取最新的 state,执行对应的每一个action
  • 对于 set 的第一个参数,如果是一个函数则会引用上一次 update 产生的结果 newState,如果不是一个参数则只更新值,此时代码里取到的 state 是还没有被更新的值,上一次 update 的结果在缓存中没有被合并到树上的 hooks 链表中。

回答问题

  • set 异步问题
    • 与 Vue 的 watch 函数调用原理类似,尽力的一次视图更新前收集多次的更新变化一次解决,这也是 setState 异步实现的理由。
    • 更新 update 任务被变成链表挂在树上,然后一次性执行更新后再进行渲染,dispatchAction 中两次state 值想等时会直接 return 埠会更新。
    • 如果 state 被用作 props,那么如果同步更新 state 则 props 不能被同步。
  • 无状态的函数组件
    • 在 class 组件中,通过维护一个 class 实例来维护状态,但函数组件没有这样的能力。所以每次函数组件被执行都是从树上取下来 hooks 复制一份并定义出新的上下文,在执行完毕后又被回收。这一点可以在源码中 useState 对于是否是第一次渲染的处理看出来。

与原理无关的tips

记得清理副作用

如果有不停止的 setInterval 或者时间并不短到忽略未完成就组件被卸载的可能,要记得清理副作用。

生疏问题,不要忘记 useEffect 的依赖值