banner
IWSR

IWSR

我永远喜欢志喜屋梦子!

React Hooks: hooks 链表

文章的 React 版本基于 v17.02 的 concurrent 模式

hooks 链表是在讲解所有 hook 前绕不开的话题,此文将以调试的角度出发分析 hooks 链表的结构与作用。

组件挂载时的链表逻辑#

首先我们试着对这段代码进行录制。

  import { useLayoutEffect, useEffect, useState } from "react";

  function UseEffectAnduseLayoutEffect() {
    const [text, setText] = useState('wawawa');

    useEffect(() => {
      console.log('useEffect create 1');
      // setText('useEffect create 1');

      return () => {
        console.log('useEffect destroy 1');
        // setText('useEffect destroy 1');
      }
    }, []);

    useLayoutEffect(() => {
      console.log('useLayoutEffect create 1');
      // setText('useLayoutEffect create 1');

      return () => {
        console.log('useLayoutEffect destroy 1');
        // setText('useLayoutEffect destroy 1');
      }
    }, []);

    return (
      <div onClick={() => { setText('hahaha') }}>{ text }</div>
    )
  }

  export default UseEffectAnduseLayoutEffect;

image

从结果中我们不难注意到 renderWithHooks 这一函数,该函数内执行了 UseEffectAnduseLayoutEffect,函数式组件作为函数被调用本身就是渲染的一环,除此之外在这个函数中也调用到了代码中的 hook。

export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  ...
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;

  let children = Component(props, secondArg); // 此处只需要关注调用UseEffectAnduseLayoutEffect的场景
  ...
  return children;
}

在 React 内根据是否存在 current 来判断是挂载还是更新是一个比较常见的操作。由于我们目前记录的是初次挂载时的操作,因此此时 ReactCurrentDispatcher.current 为 HooksDispatcherOnMount 。至于为什么需要区分挂载 / 更新,后续会介绍。

接下来我们关注在 UseEffectAnduseLayoutEffect 内调用的 useState 与 useEffect 。我们将进入构筑 hooks 链表的相关逻辑。

在 UseEffectAnduseLayoutEffect 这一例子中最先被调用的是 useState ,而对其进行断点调试会进入 mountState 。

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

但是在其代码中我们目前只需要关注 mountWorkInProgressHook ,这是用来构建 hook 对象的

每调用一次 hooks 函数都会创建一个 hook 对象,其结构如下:

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  }; // hook对象的结构 是构成 hooks链表的最小单元

  if (workInProgressHook === null) {
    // This is the first hook in the list
    /**
     * 这里可以看到,fiber 中的 memoizedState 是用来存储 hooks 链表的
    */
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    /**
     * 后续在调用其他 hook 时,会直接接入到上一个 hook 对象的后面
    */
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

上述代码中请注意 workInProgressHook 这一个指针,它反映了当前组件中调用到了哪一个 hook 函数。

当代码中的 useState、useEffect、useLayout 全部执行完之后我们会获取到一条完整的 hooks 链表。

image

此时也可以看到 workInProgressHook 确实指向了最后一个 hook 对象
image

组件更新时的链表逻辑#

录制一下触发点击事件的记录,得到以下的结果。

image

此时 ReactCurrentDispatcher.current 为 HooksDispatcherOnUpdate。因此再次进入 UseEffectAnduseLayoutEffect 时调用的 hooks 将与挂载时不同。此时再次对 useState 打断点将进入 updateState 内的 updateWorkInProgressHook

function updateWorkInProgressHook(): Hook {
  let nextCurrentHook: null | Hook;
  // 根据注释 current hook list is the list that belongs to the current fiber 可知,current hook 指向了 current fiber 树上的 hooks 链表
  // 毕竟更新操作时,必然已经存在了 current 树
  // currentHook在函数组件调用完成时会被设置为null因此可以用来确认是否是刚开始重新渲染
  if (currentHook === null) {
    /**
     * 根据注释 The work-in-progress fiber. I've named it differently to distinguish it from the work-in-progress hook.
     * currentlyRenderingFiber是一个 WIP 的 fiber
     * 因此它的 alternate 是指向其对应的 current fiber 节点
    */
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      // current节点存在,将nextCurrentHook指向current.memoizedState(hooks 链表)
      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) {
    // There's already a work-in-progress. Reuse it.
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  } else {
    // Clone from the current hook.

    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) {
      // This is the first hook in the list.
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      // Append to the end of the list.
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

代码冗长,指针又跳来跳去,看懂的最好办法还是自己调试一下。这里最重要的信息其实就是,在更新时会借 current 节点上的 hooks 链表去生成新的 hooks 链表。至于如何更新状态不在本文内介绍。

总结#

  1. 每次调用函数式组件都会生成一个 hooks 链表挂载在对应 fiber 的 memoizedState 上。
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。