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

news2024/11/19 18:28:49

参考: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>;
}

最后

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/569024.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringCloud(26):系统自适应保护实现

Sentinel 系统自适应限流从整体维度对应用入口流量进行控制&#xff0c;结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标&#xff0c;通过自适应的流控策略&#xff0c;让系统的入口流量和系统的负载达到一个平衡&#xff0c;让系统尽可…

ACP(MaxCompute篇)-MaxCompute自定义函数开发

概述 数据映射关系 自定义函数 相关资料 【MaxCompute】实现自定义UDF、UDTF详解_maxcompute 自定义函数_beautiful_huang的博客-CSDN博客 UDF UDTF UDAF 函数发布 test11_123>add jar C:\Users\zhang\Desktop\相关电子书\test\target\test-1.jar -f; OK: Resource tes…

Linux 分布式版本控制系统git

目录 什么是git&#xff1f; 安装git 创建版本库 工作区和版本库、 向版本库中添加文件 版本回退 远程操作 什么是git&#xff1f;  Git 是一个开源的分布式版本控制系统&#xff0c;用于敏捷高效地处理任何或小或大的项目。 linux是一个开源软件&#xff0c;第一版本&am…

【深入浅出Maven开发实战】「入门教程系列」带你零基础学习和开发使用Maven开发工具实战指南(实战技术总结)

Maven介绍 由于Java的生态非常丰富&#xff0c;无论你想实现什么功能&#xff0c;都能找到对应的工具类&#xff0c;这些工具类都是以jar包的形式出现的&#xff0c;例如Spring,SpringMVC、MyBatis、数据库驱动&#xff0c;等等&#xff0c;都是以jar包的形式出现的&#xff0…

华为OD机试之完美走位(Java源码)

完美走位 题目描述 在第一人称射击游戏中&#xff0c;玩家通过键盘的A、S、D、W四个按键控制游戏人物分别向左、向后、向右、向前进行移动&#xff0c;从而完成走位。 假设玩家每按动一次键盘&#xff0c;游戏任务会向某个方向移动一步&#xff0c;如果玩家在操作一定次数的键…

Eureka 心跳和服务续约源码探秘——图解、源码级解析

&#x1f34a; Java学习&#xff1a;社区快速通道 &#x1f34a; 深入浅出RocketMQ设计思想&#xff1a;深入浅出RocketMQ设计思想 &#x1f34a; 绝对不一样的职场干货&#xff1a;大厂最佳实践经验指南 &#x1f4c6; 最近更新&#xff1a;2023年5月25日 &#x1f34a; 点…

【SA8295P 源码分析】03 - SA8295P QNX Host 上电开机流程分析

【SA8295P 源码分析】03 - SA8295P QNX Host上电开机流程分析 一、阶段1 固件开机自检 (SM BIST):APPS PBL加载XBL后触发 INT_RESET进行Warm Reset二、阶段2 固件开机自检 (SM BIST):加载TZ,初始Hypervisor,启动QNX Kernel,加载并启动各子系统系列文章汇总见:《【SA8295P…

如何用前端技术打造自己的2048游戏

部分数据来源&#xff1a;ChatGPT 2048游戏规则 2048是一款数字益智类游戏&#xff0c;玩家需要通过合并数字方块来获得更高的分数。游戏的规则非常简单&#xff0c;只需要使用上下左右方向键移动数字方块&#xff0c;当两个相同数字方块碰撞时&#xff0c;会合并成一个数字方…

【KVM虚拟化】· KVM中的网络

目录 &#x1f34e;虚拟机的网络模式 &#x1f352;网络配置文件 &#x1f352;virsh查看命令 &#x1f34e;基于NAT的虚拟网络 &#x1f34e;基于网桥的虚拟网络 &#x1f34e;基于隔离的虚拟网络 &#x1f990;博客主页&#xff1a;大虾好吃吗的博客 &#x1f990;专栏地址&a…

全网最全的多模态实体识别论文列表-【原文+代码】

文章目录 写在前面的话【2017年】【2018年】【2019年】【2020年】【2021年】【2022年】【2023年】【写在最后的话】 写在前面的话 近期在梳理多模态NER相关的论文&#xff0c;因此&#xff0c;本篇文章主要是为大家整理了比较全面的聚焦于多模态实体识别任务的论文列表&#x…

Linux权限相关介绍

目录 前言 1.Linux操作系统下的两种用户 Linux权限管理 Linux文件访问对象分类 文件类型和访问权限 文件类型 基本权限 文件访问权限的相关设置方法 chmod chown chgrp umask掩码 目录权限 粘滞位 前言 权限指的就是我们对于某件事物所能够相关操作&#xff0c;而对于…

x86汇编语法基础(gnu格式)

一、寄存器 1.1 通用寄存器 一个x86-64的中央处理单元&#xff08;CPU&#xff09;包含一组16个存储64位值的通用寄存器。这些寄存器用来存储整数数据和指针。下图显示了这16个寄存器。它们的名字都以%r开头&#xff0c;不过后面还跟着不同命名规则的名字&#xff0c;这是由于…

macbook2023系统清理软件cleanmymac中文版

cleanmymac x 中文版基本都是大家首选Mac清理软件了。它集各种功能于一身&#xff0c;几乎满足用户所有的清理需求。它可以清理&#xff0c;优化&#xff0c;保养和监测您的电脑&#xff0c;确保您的Mac运行畅通无阻&#xff01;支持一键快速清理Mac&#xff0c;快速检查并安全…

opencv_c++学习(二十二)

一、凸包检测 图中左侧为边缘检测的效果&#xff0c;中间为图像经过二值化的效果&#xff0c;右图为凸包检测效果。 convexHull(lnputArraypoints, OutputArray hull&#xff0c;bool clockwise false, bool returnPoints true)points:输入的2D点集。 hull:输出凸包的顶点。…

【大学物理实验】基本测量

50分度的游标卡尺&#xff0c;最小分度为&#xff1a; A. 0.1mm B. 0.2mm C. 0.5mm D. 0.02mm 正确答案&#xff1a; D 保存游标卡尺和螺旋测微器是&#xff0c;下面说法正确的是&#xff1a; A. 游标卡尺测量位置应闭合&#xff0c;螺旋测微器小砧和螺杆间隙也应闭合 B. 游标…

PyG的Planetoid无法直接下载Cora等数据集的解决方法

问题描述&#xff1a; 在使用PyG的时候&#xff0c;通常会涉及到一些公共数据集的下载&#xff0c;由于网络问题&#xff0c;导致无法下载出现以下问题&#xff1a; 尝试了很多的方法都没有成功&#xff08;主要是个人比较菜&#xff01;&#xff09;。但是皇天不负有心人&am…

基于Springboot的高校固定资产管理系统的设计与实现(源码完整)

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据你想解决的问题&#xff0c;今天给…

1.标注自己的关键点检测数据集

1.标注自己的关键点检测数据集 1.1 labelme标注数据 labelme GitHub项目地址&#xff1a;https://github.com/wkentaro/labelme 1.1.1exe文件下载 https://github.com/wkentaro/labelme/releases 可直接下载打包好的exe文件 1.1.2python安装labelme cmd命令行中输入以下命…

极大似然估计法及其损失函数的优化方法

二分类-逻辑回归模型 1.模型函数 1&#xff09;多元线性回归函数&#xff1a; Z ^ 计算 X W T \hat{Z}_{计算} XW^T Z^计算​XWT 2&#xff09;softmax函数&#xff1a; Y ^ 模型 S i g m o i d ( Z ^ 计算 ) 1 1 e − Z ^ 计算 \hat{Y}_{模型} Sigmoid(\hat{Z}_{计算})…

继瑞吉外卖后的又一个项目——SpringBoot+Vued前后端的博客系统

文章目录 博客系统项目介绍前言项目演示前台演示后台演示 组织结构后端组织结构前端组织结构 技术选型前端技术后端技术架构图系统架构图业务架构图 模块介绍前端模块后端模块 环境搭建开发工具开发环境项目运行 未完待续结语 博客系统项目介绍 前言 本项目已开源在Gitee 后端…