• 设为首页
  • 收藏本站
  • 积分充值
  • VIP赞助
  • 手机版
  • 微博
  • 微信
    微信公众号 添加方式:
    1:搜索微信号(888888
    2:扫描左侧二维码
  • 快捷导航
    福建二哥 门户 查看主题

    React useEffect、useLayoutEffect底层机制及区别介绍

    发布者: Error | 发布时间: 2025-6-16 07:42| 查看数: 117| 评论数: 0|帖子模式



    useEffect

    useEffect 是 React 中的一个 Hook,允许你在函数组件中执行副作用操作。副作用(Side Effects)是指组件中不直接涉及渲染过程的行为,例如数据获取、事件监听、订阅、设置定时器、手动修改 DOM 等。
    基本用法:
    1. useEffect(() => {
    2.   // 执行副作用操作
    3.   // 可以是数据获取、订阅等操作
    4.   return () => {
    5.     // 可选的清理操作,清理副作用
    6.   };
    7. }, [dependencies]);
    复制代码
    不设置依赖
    1. useEffect(()=>{
    2.   //获取最新的状态值
    3. })
    复制代码

    • 第一次渲染完成后,执行
      1. callback
      复制代码
      ,等价于
      1. componentDidMount
      复制代码
    • 在组件每一次更新完毕后,也会执行
      1. callback
      复制代码
      ,等价于
      1. componentDidUpdate
      复制代码
    下面的写法可以获取到最新的状态值
    1. const Demo = function Demo() {
    2.     let [num, setNum] = useState(0),
    3.         [x, setX] = useState(100);
    4.     useEffect(() => {
    5.         // 获取最新的状态值
    6.         console.log('@1', num);
    7.     });
    8.     const handle = () => {
    9.         setNum(num + 1);
    10.     };
    11.     return <div className="demo">
    12.         <span className="num">{num}</span>
    13.         <Button type="primary"
    14.             size="small"
    15.             onClick={handle}>
    16.             新增
    17.         </Button>
    18.     </div>;
    19. };
    复制代码
    设置空数组,无依赖
    1. useEffect(()=>{  },[])
    复制代码
    只有第一次渲染完毕后,才会执行
    1. callback
    复制代码
    ,每一次视图更新完毕后,
    1. callback
    复制代码
    不再执行,「类似于
    1. componentDidMount
    复制代码

    初次渲染,打印@1 @2 ,点击按钮之后,只打印出@1
    1. const Demo = function Demo() {
    2.     let [num, setNum] = useState(0),
    3.         [x, setX] = useState(100);
    4.     useEffect(() => {
    5.         // 获取最新的状态值
    6.         console.log('@1', num);
    7.     });
    8.     useEffect(() => {
    9.         console.log('@2', num);
    10.     }, []);
    11.     const handle = () => {
    12.         setNum(num + 1);
    13.     };
    14.     return <div className="demo">
    15.         <span className="num">{num}</span>
    16.         <Button type="primary"
    17.             size="small"
    18.             onClick={handle}>
    19.             新增
    20.         </Button>
    21.     </div>;
    22. };
    复制代码
    设置多个依赖
    1. useEffect(() => {
    2.    }, [依赖项1,依赖项2,依赖项3]);
    复制代码

    • 第一次渲染完毕会执行
      1. callback
      复制代码
    • 依赖的状态值(或者多个依赖状态中的一个)发生变化,也会出发
      1. callback
      复制代码
      执行
    • 但是依赖的状态如果没有变化,在组件更新的时候,
      1. callback
      复制代码
      是不会执行
    1. const Demo = function Demo() {
    2.     let [num, setNum] = useState(0),
    3.         [x, setX] = useState(100);
    4.     useEffect(() => {
    5.         console.log('@3', num);
    6.     }, [num]);
    7.     const handle = () => {
    8.         setNum(num + 1);
    9.     };
    10.     return <div className="demo">
    11.         <span className="num">{num}</span>
    12.         <Button type="primary"
    13.             size="small"
    14.             onClick={handle}>
    15.             新增
    16.         </Button>
    17.     </div>;
    18. };
    复制代码
    返回值是一个函数
    1. useEffect(()=>{ return ()=>{
    2. //获取的是上一次状态的值
    3. //返回的函数,会在组件释放的时候执行
    4. } } )
    复制代码

    • 初始渲染之后返回一个小函数,放到链表当中
    • 如果组件更新,会通过
      1. updateEffect
      复制代码
      会把上一次返回的函数执行「可以“理解为”上一次渲染的组件释放了」
    1. const Demo = function Demo() {
    2.     let [num, setNum] = useState(0),
    3.         [x, setX] = useState(100);
    4.     useEffect(() => {
    5.         return () => {
    6.             // 获取的是上一次的状态值
    7.             console.log('@4', num);
    8.         };
    9.     });
    10.     const handle = () => {
    11.         setNum(num + 1);
    12.     };
    13.     return <div className="demo">
    14.         <span className="num">{num}</span>
    15.         <Button type="primary"
    16.             size="small"
    17.             onClick={handle}>
    18.             新增
    19.         </Button>
    20.     </div>;
    21. };
    复制代码

    总结


    useEffect的使用环境

    useEffect必须是在函数的最外层上下文中调用,不能把其嵌入到条件判断、循环等操作语句中。
    下面是错误的写法:
    1. const Demo = function Demo() {
    2.     let [num, setNum] = useState(0);
    3.    if (num > 5) {
    4.         useEffect(() => {
    5.             console.log('OK');
    6.         });
    7.     }
    8.     const handle = () => {
    9.         setNum(num + 1);
    10.     };
    11.     return <div className="demo">
    12.         <span className="num">{num}</span>
    13.         <Button type="primary"
    14.             size="small"
    15.             onClick={handle}>
    16.             新增
    17.         </Button>
    18.     </div>;
    19. };
    复制代码
    正确的应该是这样,把逻辑写在useEffect内部:
    1.   useEffect(() => {
    2.         if (num > 5) {
    3.             console.log('OK');
    4.         }
    5.     }, [num]);
    复制代码
    useEffect 中发送请求

    首先模拟一个请求
    1. // 模拟从服务器异步获取数据
    2. const queryData = () => {
    3.     return new Promise(resolve => {
    4.         setTimeout(() => {
    5.             resolve([10, 20, 30]);
    6.         }, 1000);
    7.     });
    8. };
    复制代码
    错误示例

    这样写会直接进行报错。
    1. useEffect
    复制代码
    如果设置返回值,则
    1. 返回值必须是一个函数
    复制代码
    「代表组件销毁时触发」;下面案例中,
    1. callback
    复制代码
    经过
    1. async
    复制代码
    的修饰,返回的是一个
    1. promise
    复制代码
    实例,不符合要求,所以报错!
    1. useEffect(async ()=>{
    2.    let data = await queryData();
    3.    console.log(”成功“,data)
    4. },[])
    复制代码


    用.then获取数据

    直接调用
    1. queryData
    复制代码
    ,通过
    1. .then
    复制代码
    获取数据
    1. useEffect(async ()=>{
    2.    queryData().then(data=>{
    3.    console.log(”成功“,data)
    4.       })
    5. },[])
    复制代码
    在useEffect创建一个函数

    1. useEffect
    复制代码
    返回值里创建一个函数并调用
    1. useEffect( ()=>{
    2.     const next = async()=>{
    3.      let data = await queryData();
    4.      console.log(”成功“,data)
    5.     };
    6.     next();
    7. },[])
    复制代码
    总结



    useLayoutEffect
    1. useLayoutEffect
    复制代码
    1. useEffect
    复制代码
    具有相似的 API 和用法,但它们的执行时机不同。useLayoutEffect 是 同步执行 的,它会在浏览器 绘制(paint)之前 执行副作用操作。
    基本用法:
    1. useLayoutEffect(() => {
    2.   // 执行副作用操作,特别是需要与 DOM 布局相关的操作
    3.   return () => {
    4.     // 可选的清理操作
    5.   };
    6. }, [dependencies]);
    复制代码
    useLayoutEffect 和useEffect区别
    1. useLayoutEffect
    复制代码
    会阻塞浏览器渲染真实DOM,优先执行
    1. Effect链表
    复制代码
    中的
    1. callback
    复制代码
    ;
    1. useEffect
    复制代码
    不会阻塞浏览器渲染真实DOM,在渲染真实DOM的同时,去执行Effect链表中的
    1. callback
    复制代码
    ;

      1. useLayoutEffect
      复制代码
      设置的
      1. callback
      复制代码
      要优先于
      1. useEffect
      复制代码
      去执行
    • 在两者设置的
      1. callback
      复制代码
      中,依然可以获取DOM元素「因为这是的DOM对象已经创建了,区别只是浏览器是否渲染」
    • 如果在
      1. callback
      复制代码
      函数中又修改了状态值「视图又要更新」

      • useEffect:浏览器肯定是把第一次的真实DOM已经绘制,再去渲染第二次的真实
      • DOMuseLayoutEffect:浏览器是把两次真实DOM的渲染,合并在一起渲染

    视图更新的步骤
    1、基于
    1. babel-preset-react-app把JSX便衣乘
    复制代码
    createElement`格式
    2、把
    1. createElement
    复制代码
    执行,创建
    1. virtualDOM
    复制代码
    3、基于
    1. root.render
    复制代码
    方法把
    1. virtual
    复制代码
    变为真实DOM对象「DOM- DIFF」
    1. useLayoutEffect
    复制代码
    阻塞第4步操作,先去执行Effect链表中的方法「同步操作」
    1. useEffect
    复制代码
    第4步操作和Effect链表中的方法执行,是同时进行的「异步操作」
    4、浏览器渲染和绘制真实DOM对象
    下面先打印出
    1. useLayoutEffect
    复制代码
    ,再打印出
    1. useEffect
    复制代码
    1. const Demo = function Demo() {
    2.     // console.log('RENDER');
    3.     let [num, setNum] = useState(0);
    4.     useLayoutEffect(() => {
    5.         console.log('useLayoutEffect'); //第一个输出
    6.     }, [num]);
    7.     useEffect(() => {
    8.         console.log('useEffect'); //第二个输出
    9.     }, [num]);
    10.     return <div className="demo"
    11.         style={{
    12.             backgroundColor: num === 0 ? 'red' : 'green'
    13.         }}>
    14.         <span className="num">{num}</span>
    15.         <Button type="primary" size="small"
    16.             onClick={() => {
    17.                 setNum(0);
    18.             }}>
    19.             新增
    20.         </Button>
    21.     </div>;
    22. };
    复制代码



    执行时机:浏览器渲染的关系

    useEffect:
    useEffect 是 异步 执行的,它是在 React 更新完 DOM 后(即浏览器绘制之后)执行的。浏览器渲染通常分为几个阶段:
    浏览器渲染:更新 DOM、进行布局计算、绘制页面等。
    React 执行副作用(useEffect):在页面渲染完成后,再去执行副作用。
    这种顺序意味着 useEffect 中的副作用操作不会阻塞浏览器渲染。换句话说,React 在触发 useEffect 后,会立即开始浏览器的绘制过程,所以不会影响页面的视觉展示。
    1. 举个例子,如果你使用 useEffect 来发起 API 请求,React 会等到浏览器完成渲染后,再去发起请求,不会影响渲染速度。
    复制代码
    useLayoutEffect:
    useLayoutEffect 与 useEffect 的最大区别是它会 同步执行,并且会在 DOM 更新后但在浏览器渲染(绘制)之前执行。执行顺序如下:
    React 更新虚拟 DOM 和 DOM:这一步会根据组件的变化更新页面结构。
    useLayoutEffect 执行:同步执行副作用,这时 DOM 已经更新,但浏览器还没进行绘制。
    浏览器绘制:完成页面渲染。
    这意味着 useLayoutEffect 会 阻塞 渲染,直到它执行完成后,浏览器才会进行页面渲染。因此,如果 useLayoutEffect 中执行的操作非常耗时,可能会导致页面渲染延迟,影响用户体验。

    对浏览器渲染的影响

    useEffect 的影响:
    异步执行:不会阻塞页面渲染,可以在渲染完成后执行副作用操作。
    不会影响页面视觉:由于 useEffect 在浏览器渲染完成后才执行,它不会导致页面布局变化,也不会造成视觉闪烁。
    性能优化:因为是异步执行的,所以浏览器渲染不会被卡住,页面的响应速度和流畅性得到保证。
    useLayoutEffect 的影响:
    同步执行:会在 DOM 更新后但在页面渲染之前立即执行副作用,阻塞浏览器的绘制过程。
    可能影响性能:由于同步执行,浏览器渲染必须等待 useLayoutEffect 完成,如果副作用中有复杂的操作,可能会导致页面加载时间延迟或出现白屏现象。
    影响布局计算:适合用于获取 DOM 元素的大小、位置等布局信息,因为它在页面渲染之前执行,你可以确保你拿到的是最新的、正确的布局信息。

    使用场景

    useEffect 的常见场景:
    数据获取:例如从 API 获取数据,或者发起网络请求。
    事件监听和取消订阅:如为组件添加事件监听器(例如 resize 或 scroll),并在组件卸载时移除它们。
    定时器/计时器:设置定时任务(如 setInterval 或 setTimeout),并在组件卸载时清理。
    更新状态:例如当某个副作用触发时更新组件状态,通常与 DOM 操作无关。
    例如: 通过 useEffect 实现获取数据并更新状态:
    1. useEffect(() => {
    2.   fetchData().then(data => {
    3.     setData(data);
    4.   });
    5. }, []); // 依赖空数组,表示只在组件挂载时执行
    复制代码
    在组件中监听 resize 或 scroll 事件时,useEffect 是一个常见的选择。你可以在 useEffect 中添加事件监听器,并在组件卸载时清除这些监听器。
    1. import React, { useState, useEffect } from 'react';
    2. function WindowResize() {
    3.   const [windowWidth, setWindowWidth] = useState(window.innerWidth);
    4.   useEffect(() => {
    5.     // 定义事件处理函数
    6.     const handleResize = () => {
    7.       setWindowWidth(window.innerWidth); // 更新宽度状态
    8.     };
    9.     // 在组件挂载时添加事件监听器
    10.     window.addEventListener('resize', handleResize);
    11.     // 返回清理函数,在组件卸载时移除事件监听器
    12.     return () => {
    13.       window.removeEventListener('resize', handleResize);
    14.     };
    15.   }, []); // 空数组,表示只在组件挂载和卸载时执行
    16.   return (
    17.     <div>
    18.       <p>Window width: {windowWidth}px</p>
    19.     </div>
    20.   );
    21. }
    22. export default WindowResize;
    复制代码
    使用定时器来执行某些定期操作,如每隔一定时间更新状态。
    1. import React, { useState, useEffect } from 'react';
    2. function Timer() {
    3.   const [seconds, setSeconds] = useState(0);
    4.   useEffect(() => {
    5.     // 创建定时器,每秒增加一次秒数
    6.     const intervalId = setInterval(() => {
    7.       setSeconds((prevSeconds) => prevSeconds + 1);
    8.     }, 1000);
    9.     // 清理函数,组件卸载时清除定时器
    10.     return () => clearInterval(intervalId);
    11.   }, []); // 空数组,表示只在组件挂载时设置定时器,卸载时清理
    12.   return (
    13.     <div>
    14.       <p>Seconds: {seconds}</p>
    15.     </div>
    16.   );
    17. }
    18. export default Timer;
    复制代码
    副作用可能会触发状态更新,特别是在某些条件发生变化时,比如从 API 获取数据或处理输入事件等。useEffect 在 inputValue 改变时触发,设置一个 500 毫秒的延迟,用 setTimeout 更新 delayedValue。每次 inputValue 更新时,都会清理上一个定时器,避免旧的定时器执行。这样,delayedValue 会延迟显示输入框的值,实现了一个防抖的效果。
    1. import React, { useState, useEffect } from 'react';
    2. function InputWithDelay() {
    3.   const [inputValue, setInputValue] = useState('');
    4.   const [delayedValue, setDelayedValue] = useState('');
    5.   useEffect(() => {
    6.     // 设置延迟更新的效果
    7.     const timeoutId = setTimeout(() => {
    8.       setDelayedValue(inputValue);
    9.     }, 500); // 输入后 500ms 更新 delayedValue
    10.     // 清理函数:在输入值变化时清除上一个 timeout
    11.     return () => clearTimeout(timeoutId);
    12.   }, [inputValue]); // 依赖于 inputValue,每次输入值变化时都会触发
    13.   return (
    14.     <div>
    15.       <input
    16.         type="text"
    17.         value={inputValue}
    18.         onChange={(e) => setInputValue(e.target.value)}
    19.         placeholder="Type something..."
    20.       />
    21.       <p>Delayed value: {delayedValue}</p>
    22.     </div>
    23.   );
    24. }
    25. export default InputWithDelay;
    复制代码
    useLayoutEffect 的常见场景:
    DOM 操作:需要在页面渲染之前操作 DOM(比如滚动条位置、修改样式、元素大小调整等)。
    获取布局信息:例如测量 DOM 元素的宽度、高度或位置,因为这些信息可能在浏览器绘制过程中发生变化,所以你必须在渲染之前获取。
    修复布局闪烁:如果你需要在页面渲染之前进行 DOM 操作,否则会导致闪烁或视觉不一致。
    例如: 使用 useLayoutEffect 获取 DOM 元素尺寸:
    1. import React, { useState, useLayoutEffect, useRef } from 'react';
    2. function Component() {
    3.   const [size, setSize] = useState({ width: 0, height: 0 });
    4.   const divRef = useRef(null);
    5.   useLayoutEffect(() => {
    6.     const div = divRef.current;
    7.     if (div) {
    8.       const { width, height } = div.getBoundingClientRect();
    9.       setSize({ width, height });
    10.     }
    11.   }, []); // 只在挂载时执行
    12.   return (
    13.     <div ref={divRef}>
    14.       Width: {size.width}, Height: {size.height}
    15.     </div>
    16.   );
    17. }
    复制代码
    使用 useLayoutEffect 来确保在浏览器绘制页面之前,能获取到最新的 DOM 元素的尺寸。

    性能对比


    • useEffect 的性能优势:由于是异步执行,它不会阻塞浏览器的渲染过程。即使副作用中有较重的操作(如网络请求、设置定时器等),它们也会在浏览器渲染完成后执行,不会影响页面渲染速度。
    • useLayoutEffect 的性能成本:由于它是同步执行,并且会阻塞浏览器绘制,可能会导致页面渲染的延迟,特别是在副作用操作比较复杂时(比如大量的 DOM 计算)。如果在 useLayoutEffect 中执行了复杂的逻辑,它可能会影响页面的响应速度,给用户带来不流畅的体验。
    到此这篇关于React useEffect、useLayoutEffect底层机制的文章就介绍到这了,更多相关React useEffect、useLayoutEffect内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

    来源:https://www.jb51.net/javascript/33938502n.htm
    免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有账号?立即注册

    ×

    最新评论

    QQ Archiver 手机版 小黑屋 福建二哥 ( 闽ICP备2022004717号|闽公网安备35052402000345号 )

    Powered by Discuz! X3.5 © 2001-2023

    快速回复 返回顶部 返回列表