react(二)useEffect 和 useRef

useEffect

副作用

在 React 中,副作用指的是在组件渲染过程中,除了返回 JSX 之外进行的任何操作,这些操作会影响组件外部或与外部系统进行交互。

在函数式编程和 React 上下文中:
纯函数 :相同的输入 ⇒ 相同的输出,无外部影响
副作用:函数执行过程中对外部环境产生的可观察变化

js 复制代码
// 纯函数 - 无副作用
function add(a, b) {
  return a + b; // 只进行计算,不影响外部
}

// 有副作用的函数
function updateTitle(title) {
  document.title = title; // 影响外部 DOM
  console.log(title);     // 影响外部控制台
  fetch('/api');          // 影响网络
}

常见的副作用包括:

  • 数据获取(API 调用)
  • 事件订阅(WebSocket、setTimeout、事件监听器)
  • 手动修改 DOM
  • 记录日志
  • 存储数据到 localStorage

副作用之所以特殊,是因为它们可能在不同时间执行,可能影响其他组件,并且可能导致不一致的 UI 状态。

使用 useEffect 统一管理副作用

useEffect 是 React 专门为函数组件 设计的副作用管理系统 。其核心关系是:
useEffect = 副作用声明 + 生命周期管理 + 清理机制

作用:

  • 分离关注点:将副作用逻辑与渲染逻辑分离
  • 生命周期模拟:在函数组件中模拟类组件的生命周期方法
  • 声明式副作用:通过依赖数组声明副作用执行的条件

useEffect 的执行机制详解

  1. 执行时机与浏览器渲染流程
  • 组件渲染
  • React 更新 DOM
  • 浏览器绘制屏幕
  • useEffect 执行

关键特性:

  • useEffect 的回调函数是异步执行的
  • 不会阻塞浏览器绘制
  • 布局和绘制之后执行(类似 componentDidMount和 componentDidUpdate)
js 复制代码
//  不能直接在函数体执行副作用
function Component() {
  // 错误:每次渲染都会执行
  fetch('/api/data'); 
  document.title = '新标题'; 
  
  return <div>内容</div>;
}

// 必须用 useEffect 包装
function Component() {
  useEffect(() => {
    fetch('/api/data'); // 正确执行(DOM 更新后)
    document.title = '新标题';  // 正确执行(DOM 更新后)
  }, []);
  
  return <div>内容</div>;
}

语法:

js 复制代码
useEffect(() => {
  // 副作用逻辑
  return () => {
    // 清理函数(可选)
  };
}, [dependencies]); // 依赖项数组(可选)关于依赖项下面有深度解析

副作用执行时机:在组件渲染到屏幕之后执行。这确保副作用不会阻塞浏览器绘制。

三种依赖模式
模式 A:无依赖数组

js 复制代码
useEffect(() => {
  console.log('每次渲染后都会执行');
  // 潜在性能问题:可能导致无限循环
});
  • 执行时机:每次组件渲染(包括初始渲染和每次更新)后
  • 清理时机:每次执行新副作用前,执行上一次的清理函数
  • 使用场景:极少使用,通常只在需要严格同步副作用与渲染时使用

模式 B:空依赖数组 []

js 复制代码
useEffect(() => {
  console.log('仅在挂载时执行一次');
  
  return () => {
    console.log('仅在卸载时执行清理');
  };
}, []);

执行时机:

  • 初始渲染后执行一次
  • 组件卸载时执行清理函数
    等价于:类组件中的 componentDidMount+ componentWillUnmount
    常见用途:
  • 事件监听器绑定/解绑
  • 一次性数据获取
  • 第三方库初始化

模式 C:有依赖的数组 [dep1, dep2]

js 复制代码
useEffect(() => {
  console.log('依赖变化时执行');
  // 当 count 或 name 变化时执行
  
  return () => {
    console.log('执行上一次的清理');
  };
}, [count, name]); // 依赖项

执行时机:

  • 初始渲染后执行
  • 依赖项数组中任一值发生变化时重新执行
  • 重新执行前,先执行上一次的清理函数
    依赖比较:使用 Object.is进行浅比较
    优化技巧:
js 复制代码
// 避免对象/数组作为依赖时的无限更新
const [user, setUser] = useState({ id: 1, name: 'Alice' });

// 每次渲染 user 都是新对象,会导致副作用无限执行
useEffect(() => {
  console.log(user);
}, [user]); //  不推荐

// 解决方案1:提取具体值
useEffect(() => {
  console.log(user.id);
}, [user.id]); // 推荐

// 解决方案2:使用 useMemo
const memoizedUser = useMemo(() => user, [user.id, user.name]);

useEffect 的清理机制

  1. 清理函数的作用
  • 防止内存泄漏:清除定时器、取消订阅
  • 避免状态不一致:取消未完成的异步操作
  • 资源管理:关闭连接、释放资源
  1. 清理执行的实际顺序
js 复制代码
// 示例:多个 useEffect 的执行顺序
function Component() {
  useEffect(() => {
    console.log('Effect 1 - 设置');
    return () => console.log('Effect 1 - 清理');
  }, []);
  
  useEffect(() => {
    console.log('Effect 2 - 设置');
    return () => console.log('Effect 2 - 清理');
  }, []);
  
  // 执行顺序:
  // 挂载时: Effect 1 - 设置 → Effect 2 - 设置
  // 更新时: Effect 1 - 清理 → Effect 1 - 设置 → Effect 2 - 清理 → Effect 2 - 设置
  // 卸载时: Effect 2 - 清理 → Effect 1 - 清理
}

useEffect 的实践模式

  1. 数据获取模式
js 复制代码
useEffect(() => {
  let isMounted = true; // 解决竞态条件
  
  const fetchData = async () => {
    try {
      const result = await fetch(`/api/data/${id}`);
      const data = await result.json();
      
      if (isMounted) {
        setData(data);
      }
    } catch (error) {
      if (isMounted) {
        setError(error);
      }
    }
  };
  
  fetchData();
  
  return () => {
    isMounted = false; // 清理时标记组件已卸载
  };
}, [id]);
  1. 事件监听模式
js 复制代码
useEffect(() => {
  const handleScroll = (event) => {
    console.log('滚动位置:', window.scrollY);
  };
  
  // 节流优化
  const throttledHandleScroll = throttle(handleScroll, 100);
  
  window.addEventListener('scroll', throttledHandleScroll, { passive: true });
  
  return () => {
    window.removeEventListener('scroll', throttledHandleScroll);
  };
}, []);
  1. 定时器模式
js 复制代码
useEffect(() => {
  const intervalId = setInterval(() => {
    setCount(prev => prev + 1);
  }, 1000);
  
  return () => {
    clearInterval(intervalId);
  };
}, []);

useRef

什么是 useRef?

作用:返回一个可变的 ref 对象,其 .current属性被初始化为传入的参数。它在整个组件的生命周期内保持不变。

特点

  • 返回一个可变对象,其 .current属性被初始化为传入的参数
  • 不触发组件重新渲染 当值变化时
  • 引用在组件生命周期中保持不变

常用于:

  • 直接访问和操作 DOM 元素
  • 存储不会触发重新渲染的可变值
  • 保持对某些值的持久引用

不是状态值,是储存值

基本语法

js 复制代码
import { useRef } from 'react';
const refContainer = useRef(initialValue);

主要用途

1. 访问 DOM 元素(最常见用途)

js 复制代码
import { useEffect, useRef } from 'react'

export const TextInputFocus = () => {
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    inputRef.current?.focus()
    console.log(inputRef.current); // 可以访问到input元素
  }, []) // 组件挂载后自动聚焦输入框

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button >聚焦输入框</button>
    </div>
  )
}

2. 存储可变值(不会触发重新渲染)

存储组件实例变量

js 复制代码
import { useEffect, useRef } from 'react'

export const TextInputFocus = () => {
  const inputRef = useRef<HTMLInputElement>(null)
  const isMountedRef = useRef<boolean>(false)
  

  // 组件挂载后自动聚焦输入框,并记录组件是否已挂载
  useEffect(() => {
    isMountedRef.current = true
    inputRef.current?.focus()
    console.log(isMountedRef.current);
  }, [])


  return (
    <div>
      <input ref={inputRef} type="text" />
      <button >聚焦输入框</button>
    </div>
  )
}

定时器

js 复制代码
import { useRef, useState } from 'react';

export const TimerComponent = () => {
  const [seconds, setSeconds] = useState<number>(0);
  const timerRef = useRef<number>(0); // 存储定时器ID
  
  const startTimer = () => {
    if (timerRef.current) return; // 如果已经有定时器,不重复创建
    
    timerRef.current = setInterval(() => {
    // 定时器回调,每秒执行
      setSeconds(s => s + 1);
    }, 1000);

    console.log(timerRef.current);
    
  };
  
  const stopTimer = () => {
    clearInterval(timerRef.current);
    timerRef.current = 0; // 清除引用
  };
  
  return (
    <div>
      <p>已过去: {seconds} 秒</p>
      <button onClick={startTimer}>开始</button>
      <button onClick={stopTimer}>停止</button>
    </div>
  );
}

3. 存储上一次的状态或 props

React 函数组件没有内置的方法来获取上一次渲染的值,useRef可以解决这个问题。

js 复制代码
import { useEffect, useRef, useState } from 'react';

export const PreviousValueTracker = () => {
  const [value, setValue] = useState<string>('');
  const prevValueRef = useRef<string>(''); // 存储上一个值
  // revValueRef.current = value; 不可以再渲染期间渲染

  useEffect(() => { 
  	// 注意:这里在组件渲染后才执行
    prevValueRef.current = value; // 更新上一个值
    
  }, [value]); // 依赖于 value 的变化


  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  // 更新 value,触发重新渲染
    setValue(e.target.value);
    // 这里 previousValueRef.current 还没更新!
  };

  return (
    <>
      <p>当前值: {value}</p>
      <p>上一个值: {prevValueRef.current}</p>
      <input type="text" value={value} onChange={handleChange} />
    </>
  )
}

工作原理详解

// 组件渲染过程:

// 第一次渲染: value = '', previousValueRef.current = ''

// 用户输入 "a":

// → 触发 onChange: setValue('a')

// → 组件重新渲染

// → useEffect 运行(在渲染后): previousValueRef.current = 'a'

// 第二次渲染: value = 'ab', previousValueRef.current = 'a'(但显示的是上一次的值)

使用 ref 存储状态/props 的关键要点

  1. 时机很重要:ref 的赋值应该在 useEffect中,而不是在渲染函数体中
  2. 异步更新:ref 的更新是同步的,但读取可能在 React 渲染周期中的不同时间点
  3. 不要依赖它来渲染:ref 的值变化不会触发渲染,所以不能用它来驱动 UI
  4. 组件卸载时清理:如果 ref 存储了资源(如定时器),需要在卸载时清理

与 useState 的区别

特性 useRef useState
触发重新渲染 不会
值更新时机 立即更新 在下一次渲染时更新
存储位置 组件实例 组件状态
异步更新 同步 异步

总结:如果需要值变化时触发组件重新渲染,使用 useState;如果不需要,使用 useRef。

相关推荐
maxmaxma2 小时前
ROS2机器人少年创客营:Python第一课
前端·python·机器人
RDCJM2 小时前
Spring Boot项目接收前端参数的11种方式
前端·spring boot·后端
LZQ <=小氣鬼=>2 小时前
React 插槽(Slot)
前端·javascript·react.js
方安乐2 小时前
react之通用表格组件最佳实践(TSX)
javascript·react.js·ecmascript
Z_Wonderful2 小时前
React 中基于 Axios 的二次封装(含请求守卫)
javascript·react.js·ecmascript
早點睡3902 小时前
ReactNative项目OpenHarmony三方库集成实战:react-native-shimmer-placeholder
javascript·react native·react.js
哈__2 小时前
ReactNative项目OpenHarmony三方库集成实战:react-native-splash-screen
javascript·react native·react.js
前端老石人2 小时前
HTML 内容分组终极指南:从语义化标签到现代 Web 结构
前端·html
大转转FE2 小时前
转转前端周刊第191期: 淘宝闪购 AI Agent 的秒级响应记忆系统
前端·人工智能