React:useEffect 深度解析、useLayoutEffect 深度解析、useEffect 正确处理异步请求,和避免竞态条件?

文章目录

  • [一、React 进阶笔记:useEffect 深度解析](#一、React 进阶笔记:useEffect 深度解析)
    • [1. 核心思维模型:从"生命周期"到"同步"](#1. 核心思维模型:从“生命周期”到“同步”)
      • [A. 执行时机](#A. 执行时机)
    • [2. 依赖项数组(Dependencies)的规则](#2. 依赖项数组(Dependencies)的规则)
    • [3. 清理函数(Cleanup Function)](#3. 清理函数(Cleanup Function))
  • [二、useLayoutEffect 深度解析](#二、useLayoutEffect 深度解析)
    • [1. 核心差异:执行时机](#1. 核心差异:执行时机)
    • [2. 为什么需要 useLayoutEffect?(解决闪烁问题)](#2. 为什么需要 useLayoutEffect?(解决闪烁问题))
    • [3. 代码示例:测量元素位置](#3. 代码示例:测量元素位置)
    • [4. 使用原则与建议](#4. 使用原则与建议)
      • [A. 优先使用 `useEffect`](#A. 优先使用 useEffect)
      • [B. 仅在以下情况使用 `useLayoutEffect`](#B. 仅在以下情况使用 useLayoutEffect)
      • [C. 服务端渲染(SSR)警告](#C. 服务端渲染(SSR)警告)
  • [三、useEffect 正确处理异步请求,和避免竞态条件?](#三、useEffect 正确处理异步请求,和避免竞态条件?)
    • [1. 为什么不能直接使用 `async`?](#1. 为什么不能直接使用 async?)
    • [2. 什么是竞态条件(Race Condition)?](#2. 什么是竞态条件(Race Condition)?)
    • [3. 如何避免竞态条件?](#3. 如何避免竞态条件?)
      • [方案 1:使用 Boolean 标志位(推荐)](#方案 1:使用 Boolean 标志位(推荐))
      • [方案 2:使用 AbortController(原生撤回)](#方案 2:使用 AbortController(原生撤回))

一、React 进阶笔记:useEffect 深度解析

useEffect 是 React 中最强大也最容易被误用的 Hook。它的核心目的不是"生命周期钩子",而是**"同步"**:将组件内部的状态与外部系统(API、DOM、订阅等)同步。


1. 核心思维模型:从"生命周期"到"同步"

不要用类组件的 componentDidMount 等生命周期去套用 useEffect。你应该思考的是:
"每当状态 [Dependency] 改变时,我需要重新运行这段副作用逻辑。"

A. 执行时机

  • 渲染后执行useEffect 会在浏览器完成布局与绘制(Paint)之后异步执行,因此不会阻塞屏幕更新。
  • 闭包陷阱 :每次渲染都有自己的 Props 和 State,useEffect 捕获的是对应那次渲染中的值。

2. 依赖项数组(Dependencies)的规则

依赖项决定了 Effect 是否重新执行,是调优性能和逻辑正确性的关键。

依赖项写法 执行时机 适用场景
useEffect(() => { ... }) 每次渲染后都执行 极少使用,通常会导致性能问题
useEffect(() => { ... }, []) 仅在挂载时执行一次 初始化数据、绑定全局事件、建立 WebSocket
useEffect(() => { ... }, [a, b]) a 或 b 改变时执行 搜索过滤、根据 ID 获取数据、响应状态变化

3. 清理函数(Cleanup Function)

当 Effect 返回一个函数时,React 会在组件卸载 以及下一次 Effect 执行之前调用它。

javascript 复制代码
useEffect(() => {
  const timer = setInterval(() => console.log('Tick'), 1000);

  // 清理逻辑
  return () => {
    clearInterval(timer); 
    console.log('Cleanup: 计时器已销毁');
  };
}, []);

二、useLayoutEffect 深度解析

useLayoutEffectuseEffect 的一个特殊版本。它的签名(参数)与 useEffect 完全一致,但它的执行逻辑是同步的,会阻塞浏览器的渲染。


1. 核心差异:执行时机

理解 useLayoutEffect 的关键在于它在浏览器渲染流水线中所处的位置。

特性 useEffect useLayoutEffect
执行时机 浏览器绘制(Paint)完成之后 浏览器绘制之前,DOM 更新之后
执行方式 异步(不会阻塞页面渲染) 同步(会阻塞页面渲染,直到逻辑运行完)
性能影响 极小,不会导致视觉卡顿 如果逻辑复杂,会导致明显的页面白屏或掉帧
典型场景 API 请求、事件绑定、日志统计 测量 DOM、防止 UI 闪烁

2. 为什么需要 useLayoutEffect?(解决闪烁问题)

当你需要根据 DOM 的实际尺寸、位置来更新 UI 时,如果使用 useEffect

  1. 浏览器渲染初始 UI。
  2. useEffect 执行,修改状态,导致重新渲染。
  3. 浏览器渲染更新后的 UI。
    结果:用户会看到 UI 在一瞬间发生了跳动或闪烁。

使用 useLayoutEffect

  1. React 计算出 DOM 变化。
  2. useLayoutEffect 立即执行,测量 DOM 并进行同步修改。
  3. 浏览器一次性绘制最终的结果。
    结果:用户只看到最终正确的状态,没有任何闪烁。

3. 代码示例:测量元素位置

javascript 复制代码
import React, { useState, useLayoutEffect, useRef } from 'react';

function Tooltip() {
  const [position, setPosition] = useState(0);
  const divRef = useRef();

  useLayoutEffect(() => {
    // 此时 DOM 已经挂载,但浏览器还没画到屏幕上
    const { bottom } = divRef.current.getBoundingClientRect();
    // 同步更新状态,React 会确保这一步和初始渲染合并
    setPosition(bottom + 10);
  }, []);

  return (
    <div ref={divRef} style={{ marginTop: '50px' }}>
      目标元素,Tooltip 位置:{position}px
    </div>
  );
}

4. 使用原则与建议

A. 优先使用 useEffect

99% 的场景下,你应该首选 useEffect。因为其异步执行的特性不会阻塞浏览器的屏幕渲染,能为用户提供更流畅的交互体验。

B. 仅在以下情况使用 useLayoutEffect

  • 测量布局 :需要获取 width, height, scrollPosition 等实时 DOM 属性。
  • 防止闪烁:当状态更新会立刻改变 DOM 样式(例如:下拉菜单的定位校准、动画起始位修正)。
  • 同步操作:需要在浏览器重绘(Repaint)之前,确保某些逻辑必须串行完成。

C. 服务端渲染(SSR)警告

在 Next.js 或 Remix 等 SSR 环境中,直接使用 useLayoutEffect 会收到控制台警告。

  • 原因:服务端环境没有真实的 DOM 树,无法执行同步的布局测量。
  • 解决方案
    • 优先改用 useEffect
    • 判断环境:使用"同构"自定义 Hook:const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;

一句话总结

  • useEffect 是"渲染后通知我"。

  • useLayoutEffect 是"渲染前拦住我,等我改完再画"。

三、useEffect 正确处理异步请求,和避免竞态条件?

在 React 中处理异步请求时,最容易犯的错误就是直接在 useEffect 回调函数上加 async。以下是处理异步请求的标准模式,以及解决"竞态条件"的深度方案。

1. 为什么不能直接使用 async

你可能想写 useEffect(async () => { ... }, []),但这是错误的。

  • 原因useEffect 必须返回一个 清理函数(Cleanup Function) 或者什么都不返回(undefined)。
  • 后果 :由于 async 函数隐式返回一个 Promise,这会让 React 感到困惑,导致它无法在组件卸载或重新执行 Effect 时正确识别并执行清理逻辑。
  • 正确做法:在 Effect 内部定义一个异步函数并立即调用它。
javascript 复制代码
useEffect(() => {
  const fetchData = async () => {
    const response = await fetch('/api/data');
    const result = await response.json();
    setData(result);
  };

  fetchData();
}, []);

2. 什么是竞态条件(Race Condition)?

假设你有一个搜索框,用户快速输入了 "React":

  1. 发起请求 A(关键词 "Re")。
  2. 发起请求 B(关键词 "React")。
  3. 请求 B 响应较快,先返回了,页面显示了 "React" 的结果。
  4. 请求 A 因为网络波动后返回了,页面数据被覆盖成了 "Re" 的结果(旧数据)。

此时,用户明明输入的是 "React",看到的却是 "Re" 的结果。这就是典型的竞态条件


3. 如何避免竞态条件?

解决的核心思路是:在下一次 Effect 执行前,废弃掉(或忽略)上一次未完成的请求。

方案 1:使用 Boolean 标志位(推荐)

这是最简单、最稳健的方法,利用了闭包和 useEffect 的清理函数。

javascript 复制代码
useEffect(() => {
  let isCancelled = false; // 1. 定义一个标志位

  const fetchData = async () => {
    const result = await fetch(`/api/search?q=${query}`);
    const data = await result.json();

    // 2. 在更新状态前,检查当前 Effect 轮次是否已被废弃
    if (!isCancelled) {
      setData(data);
    }
  };

  fetchData();

  // 3. 清理函数:当 query 改变或组件卸载时执行
  return () => {
    isCancelled = true; 
  };
}, [query]);

方案 2:使用 AbortController(原生撤回)

这种方法不仅能从逻辑上忽略数据,还能真正中断网络传输,从而节省带宽。

javascript 复制代码
useEffect(() => {
  const controller = new AbortController();
  const signal = controller.signal;

  const fetchData = async () => {
    try {
      const response = await fetch(`/api/data?q=${query}`, { signal }); // 传入 signal
      const data = await response.json();
      setData(data);
    } catch (error) {
      if (error.name === 'AbortError') {
        console.log('请求已被取消,忽略状态更新');
      } else {
        // 处理真实的业务错误
        console.error('请求出错:', error);
      }
    }
  };

  fetchData();

  return () => {
    controller.abort(); // 撤回未完成的请求
  };
}, [query]);
相关推荐
果壳~1 小时前
【Uniapp】【rich-text】富文本展示以及图片预览功能解决方案
前端·javascript·uni-app
z19408920661 小时前
在线生成背景:字号层级怎么做才像「正式物料」
前端·javascript·html
skilllite作者1 小时前
GEO 是什么:从搜索引擎到「对话式答案」的信息可见性
java·前端·笔记·安全·搜索引擎·agentskills
Hello--_--World1 小时前
React:useState 函数式更新、useContext 全解析、useReducer 深度解析
前端·react.js·前端框架
李白的天不白1 小时前
vue优化建议
前端·javascript·vue.js
前端老石人1 小时前
Chrome DevTools 调试入门:从零开始排查 CSS 问题
前端·css·chrome devtools
恋猫de小郭1 小时前
经典,Flutter iOS 又修复了一个构建问题,还是很抽象
android·前端·flutter
invicinble2 小时前
前端框架使用vue-cli(总篇章介绍)
前端·vue.js·前端框架
QD_ANJING2 小时前
普及一下五月AI前端面试需要达到的强度....
前端·javascript·vue.js·人工智能·面试·职场和发展
AI自动化工坊2 小时前
Chrome DevTools MCP:让AI编码代理获得浏览器调试能力
前端·人工智能·chrome devtools