React Hooks 源码解读之 useRef

2,331 阅读5分钟

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

react 版本:v17.0.3

1、useRef 简介

useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。

我们通常会使用 useRef 来绑定DOM

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

如果我们想在组件中存储某个状态值,却又不想引起组件重新渲染,那么我们可以使用 useRef 。因为 useRef 可以很方便地保存任何可变值,当 ref 对象发生变化时,不会引发组件重新渲染。

那么,useRef 是如何实现 ref 对象发生变化而不引发组件重新渲染的呢?下面,我们来看看 useRef 的源码。

2、Hook 入口

React Hooks 源码解读之Hook入口 一文中,我们介绍了 Hooks 的入口及hook处理函数的挂载,从 hook 处理函数的挂载关系我们可以得到这样的等式:

  • 挂载阶段:

    useRef = ReactCurrentDispatcher.current.useRef = HooksDispatcherOnMount.useRef = mountRef;

  • 更新阶段:

    useRef = ReactCurrentDispatcher.current.useRef = HooksDispatcherOnUpdate.useRef = updateRef;

因此,组件在挂载阶段,执行 useRef,其实执行的是 mountRef,而在更新阶段时,则执行的是 updateRef 。

3、挂载阶段

组件在挂载阶段,执行 useRef,实际上执行的是 mountRef,下面我们来看看 mountRef 的实现。

3.1 mountRef

// packages/react-reconciler/src/ReactFiberHooks.new.js

function mountRef<T>(initialValue: T): {| current: T |} {
  // 创建 hook 对象,将 hook 对象添加到 workInProgressHook 单向链表中,返回最新的 hook 链表
  const hook = mountWorkInProgressHook();
  if (enableUseRefAccessWarning) {
    if (__DEV__) {
      
      // dev 部分代码可忽略不看
      
      // Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要原来是可写的就可以改变
      // 将创建的 ref 对象密封起来,阻止向对象添加新属性
      Object.seal(ref);
      // 将 ref 对象缓存到 hook 对象的 memoizedState 属性上
      hook.memoizedState = ref;
      // 返回一个可变的 ref 对象,其属性 current 发生变化时,不会引发组件重新渲染
      return ref;
    } else {
      // 创建 ref 对象,其 current 属性初始化为传入的参数(initialValue)
      const ref = { current: initialValue };
      // 将 ref 对象缓存到 hook 对象的 memoizedState 属性上
      hook.memoizedState = ref;
      // 返回一个可变的 ref 对象,其属性 current 发生变化时,不会引发组件重新渲染
      return ref;
    }
  } else {
    // 创建 ref 对象,其 current 属性初始化为传入的参数(initialValue)
    const ref = { current: initialValue };
     // 将 ref 对象缓存到 hook 对象的 memoizedState 属性上
    hook.memoizedState = ref;
    // 返回一个可变的 ref 对象,其属性 current 发生变化时,不会引发组件重新渲染
    return ref;
  }
}

可以看到, mountRef 的实现十分简单。首先会创建一个 hook 对象,该 hook 对象将会被添加到 workInProgressHook 单向链表中,接下来将要创建的 ref 对象将会被缓存到该 hook 对象上:

const hook = mountWorkInProgressHook();

接着创建一个 ref 对象,其 current 属性初始化为传入的参数(initialValue):

const ref = { current: initialValue };

然后将 ref 对象缓存到 hook 对象的 memoizedState 属性上:

 hook.memoizedState = ref;

最后返回一个可变的 ref 对象,其属性 current 发生变化时,不会引发组件重新渲染:

return ref;

3.2 mountWorkInProgressHook

在 mountRef() 函数中,使用 mountWorkInProgressHook() 创建了一个新的 hook 对象,我们来看看它是如何被创建的:

// packages/react-reconciler/src/ReactFiberHooks.new.js

// 创建一个新的 hook 对象,并返回当前的 workInProgressHook 对象
// workInProgressHook 对象是全局对象,在 mountWorkInProgressHook 中首次初始化
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };
  
   // Hooks are stored as a linked list on the fiber's memoizedState field
  // 将新建的 hook 对象以链表的形式存储在当前的 fiber 节点memoizedState属性上

  // 只有在第一次打开页面的时候,workInProgressHook 为空
  if (workInProgressHook === null) {
    // This is the first hook in the list
    // 链表上的第一个 hook
    
    // currentlyRenderingFiber: The work-in-progress fiber. I've named it differently to distinguish it fromthe work-in-progress hook.
    
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    // 已经存在 workInProgressHook 对象,则将新创建的这个 Hook 接在 workInProgressHook 的尾部,形成链表
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

可以看到,在新建一个 hook 对象时,如果全局的 workInProgressHook 对象不存在 (值为 null),即组件在首次渲染时,将新建的 hook 对象赋值给 workInProgressHook 对象,也同时将 hook 对象赋值给 currentlyRenderingFiber 的 memoizedState 属性,如果 workInProgressHook 对象已经存在,则将 hook 对象接在 workInProgressHook 的尾部,从而形成一个单向链表。

4、更新阶段

组件在更新阶段,执行 useRef,实际上执行的是 updateRef,下面我们来看看 updateRef 的实现。

4.1 updateRef

// packages/react-reconciler/src/ReactFiberHooks.new.js

function updateRef<T>(initialValue: T): {| current: T |} {
  // 获取该 useRef 对应的hook 对象
  const hook = updateWorkInProgressHook();
  // 返回在挂载阶段缓存在 hook 对象上的 ref 对象
  return hook.memoizedState;
}

可以看到,在更新阶段,仅仅只是返回了在挂载阶段挂载在 hook 对象的 memoizedState 属性上的 ref 对象,因此当 ref 对象内容发生变化,即 current 属性发生变更时,不会引发组件重新渲染。

4.2 updateWorkInProgressHook

在 updateRef() 函数中,通过 updateWorkInProgressHook() 函数获取到了当前正在工作中的 Hook,即 workInProgressHook,我们来看看 updateWorkInProgressHook 的实现:

// packages/react-reconciler/src/ReactFiberHooks.new.js

function updateWorkInProgressHook(): Hook {
  // This function is used both for updates and for re-renders triggered by a
  // render phase update. It assumes there is either a current hook we can
  // clone, or a work-in-progress hook from a previous render pass that we can
  // use as a base. When we reach the end of the base list, we must switch to
  // the dispatcher used for mounts.

  // 获取 当前 hook 的下一个 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;
  }

  // 取下一个 hook 为当前的hook
  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.

    // 拷贝当前的 hook,作为当前正在工作中的 workInProgressHook

    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;
}

这里分两种情况:

  • 如果是在 render 阶段,则会取下一个 hook 作为当前的hook,并返回 workInProgressHook;
  • 如果是在 re-render 阶段,则在当前处理周期中,继续取当前的 workInProgressHook 做更新处理,最后再返回 workInProgressHook。

5、useRef 流程图