React 的源码与原理解读(十一):hooks 的原理

写在专栏开头(叠甲)

  1. 作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。

  2. 本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。

  3. 本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。

本一节的内容

这个章节和前面的部分相对独立,我们将来讲一下我们在 React 16.8 更新的新特性 Hooks,我们都知道 Hooks 的提出主要就是为了解决函数组件的状态问题而存在的,之前我们的 class 组件有状态,但是 function 组件没有,所以为了解决这个问题,hooks 诞生了。那么为了让我们的 hooks 挂在在整个 React 的生命周期中, React 是怎么样处理他们的呢,我们来看看这部分的内容:

Hooks 的定义

首先我们来看看hooks 的代码,他们存放在 /react/blob/main/packages/react/src/ReactHooks.js 这个位置,他们都使用 resolveDispatcher初始化了一个 dispatcher ,然后调用 dispatcher 对应的方法来实现他们的逻辑,而 resolveDispatcher 中的 dispatcher 又是从 ReactCurrentDispatcher.current 获取的:

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  return ((dispatcher: any): Dispatcher);
}

export function useContext<T>(Context: ReactContext<T>): T {
  const dispatcher = resolveDispatcher();
  if (__DEV__) {
  //....省略
  }
  return dispatcher.useContext(Context);
}

export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

回到 renderWithHooks

关于 ReactCurrentDispatcher.current ,我们需要回到我们第一次出现 hooks 相关操作的位置,也就是我们的 renderWithHooks 函数,当时我们省略了相关的内容,现在我们需要回过来讲这部分的内容:

首先我们看到 renderWithHooks 函数的开始部分,我们根据 current === null 的判断来决定我们是使用初始化的 hooksDispatcher 还是更新的 hooksDispatcher,这两个 hooks 就是我们的刚刚看到的在函数中调用的 Dispatcher ,其中每一项都包含了我们用到的各种 hooks 的操作函数

export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
    
    
  // 获取当前函数组件对应的 fiber,并且初始化
  currentlyRenderingFiber = workInProgress;
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;


  if (__DEV__) {
    
    
    // ....
  } else {
    
    
    // 根据当前阶段决定是初始化hook,还是更新hook
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  }
  // 调用函数组件
  let children = Component(props, secondArg);
    
  // 重置hook链表指针
  currentHook = null;
  workInProgressHook = null;
    
  return children;
}

const HooksDispatcherOnMount: Dispatcher = {
    
    
  ...
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  ...
};

const HooksDispatcherOnUpdate: Dispatcher = {
    
    
  ...
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  ...
};

初始化时挂载 hooks

之后我们根据我们刚刚的两个阶段,Mount 和 Update 分别来看看我们的代码逻辑,首先是 Mount 时挂载我们的 hooks,通过查看每个HooksDispatcherOnMount 中的函数,我们可以看到,他们都使用了一个叫 mountWorkInProgressHook 的函数,我们来看看这个函数的逻辑:

  • 首先它创建了一个 hook ,它的数据结构在注释已经写清楚了,每种 hooks 需要保存不一样的值,这个我们之后会详细来说,同时还会存放我们 hooks 的更新队列,这和我们上一篇的 updateQueue 是一致的,不再赘述

  • workInProgressHook 是这个函数组件中的 hooks 的链表,一个函数组件中的 hooks 的是以链表的形式保存在一起的,而这个链表会被保存在 currentlyRenderingFibermemoizedState中,这个 currentlyRenderingFiber 则是我们之前获取到的当前函数组件对应的 Fiber

function mountState(initialState) {
    
    
  const hook = mountWorkInProgressHook();
  ...
}
  
function mountWorkInProgressHook(): Hook {
    
    
  const hook: Hook = {
    
    
    memoizedState: null, // 上次渲染时所用的 state
    baseState: null,  // 已处理的 update 计算出的 state
    baseQueue: null,  // 未处理的 update 队列(上一轮没有处理完成的)
    queue: null,      // 当前的 update 队列
    next: null, // 指向下一个hook
  };

  // 保存到链表中
  if (workInProgressHook === null) {
    
    
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    
    
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

更新时的 hooks

看完了挂载时的 hooks ,我们再来看看更新,我们还是只看公共的部分:

扫描二维码关注公众号,回复: 15203629 查看本文章
  • 首先我们知道,我们 React 是有两棵树的,一颗是 Current 树表示当前展示的树,一颗是 WorkInProgress 树表示当前的构建的树,那么我们的 hooks 链表也因此分为两个 currentHook 和 workInProgressHook
  • 现在当我们要更新一个 hooks 的时候,我们首先使用 nextCurrentHook 和 nextWorkInProgressHook 来标识下一个需要操作的 hooks 。如果 nextWorkInProgressHook 存在,我们直接使用它即可,否则,我们需要从我们的 nextCurrentHook 处克隆一份我们的 hook 放入其中
function updateWorkInProgressHook(): Hook {
    
    

  // 获取 current 树上的 Fiber 的 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;
  }

  // workInProgress 树上的 Fiber 的 hook 链表
  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    
    
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    
    
    nextWorkInProgressHook = workInProgressHook.next;
  }
    
  // 如果 nextWorkInProgressHook 不为空,直接使用
  if (nextWorkInProgressHook !== null) {
    
    
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
    
    
    //否则我们克隆一份 hooks
    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;
}

总结拓展 & 经典面试题

根据上面的函数我们也可以看到,当我们更新一个 hooks 的时候,我们需要遍历它的 Current 树和 WorkInProgress 树的 hooks 链表,所以它的两个链表标识的 hooks 顺序必须一致,这也引申出了一个我们常见的面试题 —— 为什么需要将hook函数放在函数组件的顶层

我们试想这样一种情况:如果有一个 hook 在循环条件或者 if 语句中产生,当我们挂载时和我们更新时,两次的执行环境不同导致了,导致了两次产生的 hook 不同或者顺序不同,但是我们还是按照我们预想的顺序对我们的两个链表进行遍历,最后导致的结果就是,我们错误的复用了另一个 hook 作为我们当前需要复用的 hook ,从而产生了不可预计的结果,一个例子如下:

我们设置两个 useState 我们让我们的第一个 hook 在执行一次后不再执行

let isMount = false;
if(!isMount) {
    
    
    const [num, setNum] = useState('value1'); // useState1
    isMount = true;
}
const [num2, setNum2] = useState('value2'); // useState2

那么我们在第一次调用 ,也就是 mount 的时候得到的链表是 : useState1 – useState2

显然我们在更新的时候的链表应该是 useState2

但是根据我们上文的代码,我们在更新的时候,需要复用一个 hook 作为我们的 useState2,此时我们的 nextCurrentHook 指向的是链表的第一个 hook 也就是 useState1

最后整个逻辑就乱套了,所以我们要牢记 需要将hook函数放在函数组件的顶层,不能在循环、条件或嵌套函数中调用 Hooks

hooks 的使用和后续

我们现在已经知道了我们 hooks 的逻辑,之后的几篇里面,我希望可以讲解一些常用的 hooks 的原理和源码的解析,大致包括:

  • 最常用的 useStateuseEffect
  • 性能优化 useMemouseCallback
  • 获取元素 useRef

可能还包括一些其他的和自定义 hooks 的内容,大家可以持续关注

猜你喜欢

转载自blog.csdn.net/weixin_46463785/article/details/130566356
今日推荐