Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React Hooks: hooks 链表 #2

Open
IWSR opened this issue Aug 26, 2022 · 0 comments
Open

React Hooks: hooks 链表 #2

IWSR opened this issue Aug 26, 2022 · 0 comments

Comments

@IWSR
Copy link
Owner

IWSR commented Aug 26, 2022

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 上。
@IWSR IWSR changed the title React hooks: hooks 链表 React Hooks: hooks 链表 Aug 30, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant