React Hooks useRef 源码解读+最佳实践

参考:https://juejin.cn/post/7027949526170206239

入口

下篇文章有入口源码详解,想看的可以跳转过去,这里就不放源码了,简单梳理一下流程吧

React Hooks useState 使用详解+实现原理+源码分析

流程

beginWork 判断组件类型,指挥交通,各行其道,函数组件走函数组件的道 updateFunctionComponent

updateFunctionComponent 起作用的重点函数就是我们常谈的 renderWithHooks

renderWithHooks 主要做了两件事:

  1. 用变量 currentlyRenderingFiber记录当前的 fiber node。使得 hook 能拿到当前 node的状态。
  2. 判断 hook api 挂载在那个对象上。首次渲染和后期的更新,挂载的对象是不同的 => 解耦

入口流程图

useRef 源码解析

入口流程图

声明阶段

获取并返回useRef函数

export function useRef<T>(initialValue: T): {current: T} {
  // 通过resolveDispatcher获取当前的Dispatcher
  const dispatcher = resolveDispatcher();
  return dispatcher.useRef(initialValue);
}

mount阶段

调用 mountRef,创建hookref对象,仅用四步完成ref对象的创建,并把hook添加到链上

function mountRef<T>(initialValue: T): {current: T} {
  // 第一步:创建 hook 对象,将 hook 对象添加到 workInProgressHook 单向链表中,返回最新的 hook 链表
  const hook = mountWorkInProgressHook();
  // 第二步:创建 ref 对象,其 current 属性初始化为传入的参数(initialValue)
  const ref = {current: initialValue};
  // 第三步:将 ref 对象缓存到 hook 对象的 memoizedState 属性上
  // 例如useRef(0),memoizedState即{current: 0}
  hook.memoizedState = ref; 
  // 第四步:返回一个可变的 ref 对象,其属性 current 发生变化时,不会引发组件重新渲染
  return ref;
}

mountWorkInProgressHook 内部逻辑也是很清晰的(在删除DEV环境逻辑后)

// workInProgressHook 是全局对象,在 mountWorkInProgressHook 中首次初始化;
// workInProgressHook 将被添加到 work-in-progress fiber 中的 hook 链表
// workInProgressHook hook 链表中的一个重要指针 => 它通过记录当前生成(更新)的 hook 对象,
//   可以间接反映在组件中当前调用到哪个 hook 函数了。每调用一次 hook 函数,就将这个指针的指向移到该 hook 函数产生的 hook 对象上。
let workInProgressHook: Hook | null = null; 

/**
 * 创建一个新的 hook
 * @returns 返回当前 WorkInProgressHook
 */
function mountWorkInProgressHook(): Hook {
   // 创建hook对象
  const hook: Hook = {
    memoizedState: null, // 指向当前渲染节点fiber,存储上一次更新后的最终状态
    baseState: null, // 初始化状态,每次dispatch后的newState
    baseQueue: null, // Update<any, any> 当前需要更新的,每次更新完,会赋值上一个update,方便react在渲染错误的边缘,进行错误回溯
    queue: null, // UpdateQueue<any, any> 缓存的更新队列,存储多次更新行为
    next: null, // link到下一个hook,通过next串联所有的hooks
  };

  // 只有在第一次打开页面的时候,workInProgressHook 为空
  if (workInProgressHook === null) {
    // 链表上无hook, 初始化链表的第一个hook
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // 链表上有hook,将新创建的这个hook加在链表尾部
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

workInProgressHookhook 链表中的一个重要指针 => 它通过记录当前生成(更新)的 hook 对象,可以间接反映在组件中当前调用到哪个 hook 函数了。每调用一次 hook 函数,就将这个指针的指向移到该 hook 函数产生的 hook 对象上。

update阶段、Rerender阶段

调用 updateRef 获取获取当前 useRef,然后返回 hook 链表上缓存下来的值。

也就是无论函数组件怎么执行,执行多少次,hook.memoizedState内存中都指向了一个对象,所以也解释了在useEffect,useMemo 等函数中,为什么useRef不需要依赖注入,就能访问到可变且不刷新页面的最新值。

/**
 * 仅仅返回在挂载阶段挂载在 hook.memoizedState 上的 ref 对象
 * 因此当 ref 对象内容发生变化,即 current 属性发生变更时,不会引发组件重新渲染
 * @param {*} initialValue 
 * @returns 
 */
function updateRef<T>(initialValue: T): {current: T} {
 // 通过 updateWorkInProgressHook() 函数获取该 useRef 对应的当前正在工作的 Hook
  const hook = updateWorkInProgressHook();
  // 返回在挂载阶段缓存在 hook 对象上的 ref 对象
  return hook.memoizedState;
}
/**
 * 取出current fiber中的hooks链表中对应的hook节点,挂载到workInProgress上的hooks链表
 * 用于在 updates 和 re-renders 阶段触发的重新渲染
 * @returns 返回当前 workInProgressHook
 */
function updateWorkInProgressHook(): Hook {
  // 迭代 current fiber 链表
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    // 通过alternate备份指针,复用 老的Fiber Hook链表
    const current = currentlyRenderingFiber.alternate; // 备份指针
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    // 获取 当前hook 的下一个 hook
    nextCurrentHook = currentHook.next;
  }
 
  // 迭代 workInProgress fiber 链表
  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    // workInProgressHook === null 说明是首次创建
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    // 非首次创建,取下一个 workInProgress Hook 为 当前hook
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    // 只有 re-render 的情况下,nextWorkInProgressHook 不为 null,
    // 因为在之前的 render 过程中已经创建过 workInProgress hook了,此时直接复用
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  } else {
    // 防患于未然
    if (nextCurrentHook === null) {
      const currentFiber = currentlyRenderingFiber.alternate;
      if (currentFiber === null) {
        // update阶段,currentFiber不可能为null,正常情况下应该走不到这个branch
        throw new Error(
          'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
        );
      } else {
        // update阶段,nextCurrentHook不可能为null,正常情况下应该走不到这个branch
        throw new Error('Rendered more hooks than during the previous render.');
      }
    }

    // 重新定位 正在工作中的workInProgressHook
    currentHook = nextCurrentHook;

    // 同mountWorkInProgressHook主要逻辑
    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;
}

因为renderWithHooks 函数中会执行如下代码: workInProgress.memoizedState = null,所以在执行上述函数时,有两种情况

  • currentlyRenderingFiber.memoizedState 为 null,需要从 current fiber 对应的节点中取 clone 对应的 hook,再挂载到 workInProgress fiber 的 memoizedState 链表上;

  • re-render 的情况下,由于已经创建过了 hooks,会复用已有的 workInProgress fiber 的 memoizedState。

这里正好提到,为什么 hook 不能用在条件语句中?

因为如果前后两次渲染的条件判断不一致时,会导致 current fiber 和 workInProgress fiber 的 hooks 链表结点无法对齐。

在这个函数中通过 currentlyRenderingFiber.alternate 备用指针复用 currentFibermemorizedState, 老的 Fiber Hook 链表,并且按照严格的对应顺序来复用 老的 Fiber Hook 链表中的 Hook,通过尽可能的复用来创建新的 Hook 对象,构建新的 Hook 链表。

useRef 特点

1. 渲染周期之间共享数据,即在组件重新渲染之间,引用的对象是同一个
2. 更新引用不会触发组件重渲染
3. 同步更新,更新后立即可用

用途:存储组件的基础数据,存储不需要展示在页面上的数据

最佳实践

场景1:访问dom元素 <= 最常见的使用场景

// 与浏览器监听相结合
import React, { useRef } from 'react';

function Example() {
  const divElement = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const handleScroll = () => {
      console.log('Page is scrolling');
    };

    divElement.current?.addEventListener('scroll', handleScroll, true);
    return () => {
      divElement.current?.removeEventListener('scroll', handleScroll, true);
    }
  }, [handleScroll]);

  return (
    <div ref={divElement}>
      <p>Scroll down to see the event log.</p>
    </div>
  );
}

场景2:存储定时器变量

import { useRef } from 'react';

function Example() {  
  const timer = useRef<NodeJS.Timeout>();

  const onOpenMenu = () => {
    timer.current = setTimeout(() => { /* ... */ }, 100);
  };

  const onCloseMenu = () => {
    clearTimeout(timer.current);
  };

  return <div>...</div>;  
}

场景3:解决 useState 异步更新问题

useState 存储直接呈现在屏幕上的信息

1. 更新会触发组件的重渲染,不能存储跨渲染周期的数据
2. 异步更新
    - 更新后立即使用可能出错
        - 解决方案:`setCount(prevCount => prevCount + 1)}`
            - ahooks ` useLatest  `和 `useMemoizedFn` 即是依赖 useRef 实现的
    - 在闭包内使用可能出错【闭包陷阱】
        - 存储数据,在定时器内使用
        - 问题原因:定时器一直都没有被清除,上下文环境都是第一次创建本函数式组件的上下文,
                  因此获取到的是定时器被创建时的state
        - 解决方案:用useRef保存一份state数据,定时器里使用ref.current
import { useRef, useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);
  const lastCount = useRef(count);

  useEffect(() => {
    const timer = setInterval(() => {
      console.log('last count: ', lastCount.current);
    }, 1000);
   return ()=>{
     clearInterval(timer);
   }
  }, []);

  const addCount = () => {
    setCount((val) => val + 1);
    lastCount.current += 1;
  };

  return <button onClick={addCount}>Add Count</button>;
}

场景四:🔥🔥🔥一定要注意ref的第一个特点🔥🔥🔥

=即在组件重新渲染之间,引用的对象是同一个=

如果给ref赋值了一个对象initObj,并且后面对ref进行了修改,那么initObj也会跟随变化

导致initObj已经不是最初的initObj了,这里要注意哦,很容易写bug

最好的写法是👇👇👇

const initData = useRef<>({...initObj}); // 解构一下,让ref不直接引用 initObj

最后

交流:你从 useRef 源码中发现了哪些设计思想或原则?

结束语:希望本次分享能带给你新的视野,或者帮你回忆起被遗忘在角落的记忆

React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED一个有意思的变量,感兴趣可以研究一下

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值