react中的useCallback内部实现

1,018 阅读1分钟

简介

前几天有人问我在useCallback函数如果第二个参数为空数组, 为什么拿不到最新的state值。正好自己也想多了解一下react底层实现。那么这一章就来分析一下useCallback内部是如何实现的。

示例demo与debug

新建了一个react项目,将APP.tsx改写成如下代码

import  { useCallback, useState } from 'react';

function App() {
  const [num, updateNum] = useState(0);

  const TestCallback  = useCallback(() =>{
    console.log('num: ', num);
  },[]);

  return (
    <div className="App">
        <p onClick={() => {
          updateNum(num => num + 1);
          updateNum(num => num + 1);
          updateNum(num => num + 1);
        }}>{num}</p>

        <p onClick={TestCallback}>打印</p>
    </div>
  );
}

export default App;

Vscode调试: 配置Vscode的launch.json如下。在terminal运行npm start启动项目完成再执行F5启动VScode调试

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Launch Chrome",
            "request": "launch",
            "type": "chrome",
                "url": "http://localhost:3000", // 加载的路由
                "webRoot": "${workspaceFolder}" // 代码的根目录(例如CRA创建的项目在src目录下)
        }
    ]
}

image.png

Chrome调试: 在浏览器的source设置断点,熟悉一遍useCallback的调用流程。(由于.gif过大,这里就不上git了,自行调试) image.png

源码解析

useCallback的整体流程框架

在react中mount阶段update阶段进入到同一个useCallback方法里。但resolveDispatcher找到的dispatch对象mountupdate会不同,最终导致在mount阶段调用mountCallback而update阶段调用的是updateCallback
下面为调用useCallback方法触发的行为

function useCallback(callback, deps) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useCallback(callback, deps);
}

下面来看看resolveDispatcher是如何获取到dispatch的

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

ReactCurrentDispatcher.current会在renderWithHooks方法中进行所处阶段判断并且赋值(render阶段beginWork时,会调用renderWithHooks)。如果current === null || current.memoizedState === null为true表示在mount阶段反正为update阶段

function renderWithHooks<Props, SecondArg>(...) {
     ...
     ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
}

// mount阶段调用的dispatch
const HooksDispatcherOnMount: Dispatcher = {
  ...
  useCallback: mountCallback,
};

// update阶段调用的dispatch
const HooksDispatcherOnUpdate: Dispatcher = {
  ...
  useCallback: updateCallback,
};

从上面的代码分析可以知道在mounted阶段调用的是mountCallbackupdate阶段调用updateCallback

Hook

一个函数式组件链路: fiber(FunctionComponent) => Hook(保存数据状态) => Queue(更新的队列结构) => update(更新的数据)
在后续需要使用到Hook这个结构,那么先来看一下Hook是数据结构是怎么样的,以及属性的作用是什么?

  1. memoizedState 存放的是Hook对应的state
  2. next链接到下一个Hook,从而形成一个无环单向链表
  3. queue存储同一个hook更新的多个update对象,数据结构为环状单向链表
// 组件对应的fiber对象
const fiber = {
  // 保存该FunctionComponent对应的Hooks链表
  memoizedState: hook,
  ...
};

const hook: Hook = {
    // 1.  memoizedState 存放的是Hook对应的state
    memoizedState: null,
    // 2. next链接到下一个Hook,从而形成一个`无环单向链表`
    queue: null,
    // 3. queue存储同一个hook更新的多个update对象,数据结构为`环状单向链表`  
    next: null,
    ...
};
  • fiber与Hooks的关系(懒得画图了,引用了Understanding the Closure Trap of React Hooks) 1_T1TiRZM4ilPXV4B2m2GWwA.webp

mount阶段

分析mountCallback的实现

  1. 通过mountWorkInProgressHook获取到对应的Hook对象
  2. 判断条件deps是否为undefined
  3. 回调函数判断条件存入到hook.memoizedState
  4. 返回传入的回调函数
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;
}

mountWorkInProgressHook的实现,创建初始化Hook对象,并且将该Hook对象保存在workInProgressHook链路中. workInProgressHook表示正在执行的hook

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

    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

在组件render时,每当遇到下一个Hook,通过移动workInProgressHook的指针来获取到对应的Hook
PS: 只要每次组件renderuseState的调用顺序及数量保持一致,那么始终可以通过workInProgressHook找到当前useState对应的hook对象

// fiber.memoizedState标识第一个Hook
workInProgressHook = fiber.memoizedState;
// 在组件`render`时,遇到下一个hook时
workInProgressHook = workInProgressHook.next;
....

update阶段

分析updateCallback的实现

  1. 通过updateWorkInProgressHook获取到当前的Hook对象
  2. hook.memoizedState获取到上一次缓存的state。假设这是第一次update那么其值就是mount阶段保存的[callback, nextDeps]数据
  3. 如果依赖条件不为空,使用areHookInputsEqual判断依赖项是否更改。只会遍历数组第一层数据比较不会做深层比较。如果依赖项没变化,返回原本缓存的callback
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;
}

依赖比较areHookInputsEqual的方法实现

function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
): boolean {
  ...
  // $FlowFixMe[incompatible-use] found when upgrading Flow
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    // $FlowFixMe[incompatible-use] found when upgrading Flow
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

总结

在React中会使用闭包机制来处理上文的callback回调函数。当包含useCallback组件被渲染时,React 会为该特定渲染周期创建一个闭包。闭包是一个封装的作用域,其中包含渲染时位于作用域内的变量、函数和其他引用
因此deps我们传入的是空数组,其回调函数callback一直引用的状态始终是初始状态,无法获取最新状态。缓存的回调函数可以访问最初调用时范围内的状态和道具

插件推荐

阅读源码可以通过使用Bookmarks快速标记代码位置,实现快速跳转 image.png

参考文献:

Understanding the Closure Trap of React Hooks
React技术揭秘
build-your-own-react
deep-dive-how-do-react-hooks-really-work(闭包机制的实现)