React18-Hooks初识(一):useEffect执行机制+清理副作用🤔🤔

2,252 阅读4分钟

学子不才,初学useEffect ,写了篇文章进行总结useEffect执行机制+清理副作用。如果有理解不当,还请大佬轻声指点

先聊聊副作用

在 React 中,副作用是指那些影响组件之外环境与组件渲染之外的行为相关联的操作。React 的组件渲染是纯函数(即相同的输入总是产生相同的输出),但应用程序需要与外部世界交互,比如网络请求、订阅、DOM 操作等,这些都被称为副作用。

React 提供了 useEffect 钩子,专门用来管理和处理副作用。清理副作用可以帮助开发者管理资源、避免潜在的内存泄漏,以及确保副作用在适当的时机结束或重新初始化

01| 什么是 useEffect?

useEffect是 React 提供的一个 Hook,用于处理组件中的副作用。在组件渲染完成后执行,不是因为组件事件触发执行。可以理解为一个声明副作用的工具。【副作用通常是指那些与 React 渲染流程无直接关系的操作,例如数据获取、订阅事件或直接操作 DOM。】

image.png

其基本使用方式如下:

useEffect(() => {
    // 在这里执行副作用操作
}, [依赖项数组]);

在这个例子中,useEffect 接受两个参数:

  1. 一个回调函数,用于声明副作用逻辑。
  2. 一个依赖项数组,决定了副作用的执行时机。

02| useEffect 的执行机制(!!核心🤓🤓🤓)

依赖项数组是 useEffect 的核心,它用于控制副作用函数的执行时机。当数组中的值发生变化时,React 会重新运行对应的副作用函数。根据传入的依赖项数组,useEffect 的执行行为会有所不同:

  1. 没有依赖项:在这种情况下,每次组件渲染时都会执行副作用。

    useEffect(() => {
        console.log('组件初次渲染和更新时都会执行');
    });
    
  2. 依赖项数组为空: 如果依赖项数组为空,副作用只会在组件挂载时执行一次。这种模式常用于初始化操作,例如数据获取。

    useEffect(() => {
        console.log('仅在组件初次渲染时执行');
    }, []);
    
  3. 指定依赖项: 当依赖项数组中指定的某个依赖发生变化时,副作用函数会重新执行。这种模式适合处理依赖于状态或属性变化的逻辑。

    useEffect(() => {
        console.log('依赖项变化时执行');
    }, [依赖项]);
    

一张图总结一下: image.png

03| 清理副作用

上文说过,副作用是React执行一些渲染无关的操作,比如:获取数据,订阅事件等。这时候 useEffect 的回调函数可以返回一个清理函数,用于在组件卸载或依赖项变化之前执行清理工作:

useEffect(() => {
    const timer = setInterval(() => {
        console.log('定时器执行中');
    }, 1000);

    // 返回的函数用于清理
    return () => {
        clearInterval(timer);
        console.log('定时器已清除');
    };
}, []);

1,应用场景示例

依赖性为空数组。 假设你需要发起一个网络请求,从服务器获取频道数据,并将其存储在组件的 list 状态中,然后通过 map() 方法将这些频道数据渲染为一个列表.可以这样处理:

import React, { useState, useEffect } from 'react';
const URL = 'http://example.cn'
function App() {
    const [list,setList] = useState([])
    useEffect(() => {
       // 额外操作 ,后面为空数组,只执行一次
      async function getData() {
        const res = await fetch(URL)
        const jsonRes = await res.json()
        console.log(jsonRes);
        // 将 `channels` 数组赋给 `list` 状态
         setList(jsonRes.data.channels)
      }
      // 调用   
      getData()
    },[])
    return (
        <>
           <div>
              <h2>App组件</h2>
              <ul>
                {
                  list.map(item => {
                     return <li key={item.id} >{item.name}</li>
                  })
                }
              </ul>
           </div>
        </>
    )
}
image.png

2,响应依赖变化

当依赖项是动态的时,需要将其添加到依赖项数组中:

const [stateValue, setStateValue] = useState(0);

useEffect(() => {
    getData(stateValue); // 每次 stateValue 变化时执行
}, [stateValue]);

这样可以确保每次 stateValue 变化时都能调用最新的 getData 函数。

04| 注意事项

  • 依赖项的完整性: 如果 useEffect 中使用了组件中的状态或属性,必须将其添加到依赖项数组中,避免潜在的逻辑问题。

下面以监听窗口大小变化进行计算,在useEffect内部引用count进行log打印,读者可以试试看看效果

useEffect(() => {
    const handle = () => {
        console.log(`当前计数值: ${count}`);
    };
    window.addEventListener('resize', handle);

    return () => {
        window.removeEventListener('resize', handle);
    };
}, []); // ⚠️ 如果未将 `count` 添加到依赖项数组中,可能会引用旧值。
  function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const handle = () => {
      console.log(`当前计数值: ${count}`);
    };

    window.addEventListener('resize', handle);

    // 清除事件监听器
    return () => {
      window.removeEventListener('resize', handle);
    };
  }, [count]);  // 在这里将 count 添加为依赖项,确保每次 count 更新时重新绑定事件

  return (
    <>
      <button onClick={() => setCount(count + 1)}>增加计数</button>
    </>
  );
}

结果:控制台上,resize监听到窗口变化,点击button按钮,观察打印:前者count每次点击按钮后还是为初始值0,后者打印出窗口变化次数。

总结

  • 没有依赖项的 useEffect:每次渲染时都会重新执行,并且会引用当时的状态值。这可能导致闭包捕获了一个“过时”的值,无法反映最新的状态。(为了避免“闭包陷阱”,使用外部值需要先确保状态是最新的)

  • 带依赖数组的 useEffect:确保 useEffect 在依赖的状态更新时才重新执行,这样它能够访问到最新的状态值。(也带来性能问题,所以尽量不引入不必要的依赖性,减少重新执行)

通过合理地使用 useEffect,可以有效地处理副作用操作,使代码更加直观和易维护。