本网站(662p.com)打包出售,且带程序代码数据,662p.com域名,程序内核采用TP框架开发,需要联系扣扣:2360248666 /wx:lianweikj
精品域名一口价出售:1y1m.com(350元) ,6b7b.com(400元) , 5k5j.com(380元) , yayj.com(1800元), jiongzhun.com(1000元) , niuzen.com(2800元) , zennei.com(5000元)
需要联系扣扣:2360248666 /wx:lianweikj
react中的useCallback内部实现
chenguangming9 · 118浏览 · 发布于2023-07-04 +关注

简介

前几天有人问我在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;

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

  image.png

源码解析

useCallback的整体流程框架

在react中mount阶段和update阶段进入到同一个useCallback方法里。但resolveDispatcher找到的dispatch对象mount和update会不同,最终导致在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方法中进行所处阶段判断并且赋值。如果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阶段调用的是mountCallback在update阶段调用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: 只要每次组件render时useState的调用顺序及数量保持一致,那么始终可以通过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


相关推荐

使用SELECT语句检索数据

奔跑的男人 · 807浏览 · 2019-06-03 09:33:43
部署MySQL延迟从库的几个好处

吴振华 · 666浏览 · 2019-05-14 21:57:51
MongoDB凭什么跻身数据库排行前五?

iamitnan · 724浏览 · 2019-06-18 10:04:56
Oracle开启和关闭的几种模式

qq2360248666 · 755浏览 · 2019-06-04 10:18:47
加载中

0评论

评论
分类专栏
小鸟云服务器
扫码进入手机网页