React

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > React > react useCallback内部实现

详解react中useCallback内部是如何实现的

作者:AKclown

前几天有人在问在useCallback函数如果第二个参数为空数组, 为什么拿不到最新的state值,那么这一章就来分析一下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了,自行调试)

源码解析

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方法中进行所处阶段判断并且赋值。如果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是数据结构是怎么样的,以及属性的作用是什么?

// 组件对应的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)

mount阶段

分析mountCallback的实现

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的指针来获取到对应的HookPS: 只要每次组件renderuseState的调用顺序及数量保持一致,那么始终可以通过workInProgressHook找到当前useState对应的hook对象

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

update阶段

分析updateCallback的实现

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快速标记代码位置,实现快速条件

到此这篇关于详解react中useCallback内部是如何实现的的文章就介绍到这了,更多相关react useCallback内部实现内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文