网上书城网站建设目的,做网站引流,装修房子的风格设计图软件,使用html制作网页在 React 开发中#xff0c;有时候会听到“副作用”这个词。特别是用到 useEffect 这个 Hook 的时候#xff0c;官方就明确说它是用来处理副作用的。那什么是副作用#xff1f;为什么我们要专门管控它#xff1f;今天就聊聊 React 中的组件副作用。 #x1f4cc; 什么是“…在 React 开发中有时候会听到“副作用”这个词。特别是用到useEffect这个 Hook 的时候官方就明确说它是用来处理副作用的。那什么是副作用为什么我们要专门管控它今天就聊聊 React 中的组件副作用。 什么是“副作用”其实“副作用”并不是 React 特有的东西在原生 JS 里也很常见。副作用的”反义词“是纯函数。纯函数的意思是相同的输入永远得到相同的输出且不影响函数外部的任何状态。举个例子functionadd(a,b){returnab}上面这个就是纯函数无论执行多少次add(1, 2)输出的结果永远是3。那么什么是副作用呢letcount0;functionadd(){count1;// 修改了外部变量 count产生了副作用returncount;}add()// 1add()// 2这里add()除了返回值之外还改变了count这个函数外部的变量就算是副作用。每次执行它都会得到不一样的结果这就是副作用。 常见的副作用行为包括修改全局变量 / 外部变量修改对象属性引用类型发起网络请求HTTP 请求定时器setInterval操作本地存储localStorage.setItem操作 DOM读写文件在 Node.js 里使用Date.now()、Math.random()这种非确定性函数等… 为什么要管理副作用如果不合理管理副作用React 应用可能会遇到内存泄漏重复订阅异步任务未清理数据竞争问题UI 不一致副作用是和组件生命周期息息相关的所以 React 提供了useEffect来专门管理副作用行为。‍♂️ 错误示范这个例子展示了一个定时器功能每隔1秒就会更新一下当前时间并在页面中展示。import { useState } from react; function App() { const [dateTime, setDateTime] useState(new Date()); const id setInterval(() { setDateTime(new Date()); } , 1000); console.log(id); return ( div{dateTime.toLocaleString(zh-CN)}/div ) } export default App;如果只看页面展示的情况看上去是没问题的。但打开控制台一看的话会发现随着时间的推移每秒输出的id数量会增加。这个例子的问题在于当组件的state发生变化时整个组件的代码都会重新执行一次这就相当于反复执行了setInterval好多次。当这个页面运行时间长了就会导致内存溢出。 useEffect管理副作用的标准方案使用useEffect可以解决上面「错误示范」的副作用问题。import { useEffect, useState } from react; function App() { const [dateTime, setDateTime] useState(new Date()); useEffect(() { const id setInterval(() { setDateTime(new Date()); } , 1000); console.log(id); }, []) return ( div{dateTime.toLocaleString(zh-CN)}/div ) } export default App;从上面的例子可以看出页面的值是会变化的但控制台并不会一直打印id。useEffect可以传入2个参数第一个参数是要执行的代码第二个参数可以传入一个空数组。这样useEffect里的代码就会在组件第一次加载的时候执行一次因为里面执行的是一个定时器函数所以定时器函数会自己继续执行更新完页面后也不会再次执行新的定时器。如果没传入第二个参数的话就和上面的「错误示范」的效果是一样的。⏰ useEffect 的执行时机在使用函数式组件时结合useEffect函数组件的生命周期只需关注以下几个组件第一次加载组件重新渲染更新组件卸载useEffect就是在以上时机里做一些副作用。useEffect不传第二个参数时意味着它会在组件每次state或者props发生变化时执行一次。而第二个参数时空数组时意味着它不依赖与任何state或者props的变化只在组件第一次加载时执行它的副作用里面的函数。如果第二个参数是数组且依赖其他状态的话那么其依赖的其中一个状态发生变化时useEffect里的代码都会重新执行一次。关于最后一点举个例子 。import { use } from react; import { useEffect, useState } from react; function App() { const [count, setCount] useState(0); const [doubleCount, setDoubleCount] useState(0); useEffect(() { setDoubleCount(() { return count * 2; }); } , [count]); return ( div div{doubleCount}/div button onClick{() setCount(count 1)}Increment/button /div ) } export default App;在这个例子中useEffect的第二个参数是[count]表示这个useEffect依赖了count当count发生变化时就会执行useEffect第一个参数的函数。前面也提到useEffect的第二个参数可以依赖多个状态当其中一个状态发生变化时也会执行第一个参数的代码。可以自己手动试试 清理副作用在副作用函数中返回一个函数用于在下一次执行副作用之前或组件卸载时进行清理。示例1只在挂载/卸载时清理因为依赖数组是空的effect 执行一次后就不再重新运行。返回的清理函数只会在组件卸载时被调用一次。import React, { useEffect } from react; function Countdown({ start }) { useEffect(() { const timerId setInterval(() { console.log(倒计时, new Date().toLocaleTimeString()); }, 1000); // 清理函数只在组件卸载时调用 return () { clearInterval(timerId); console.log(倒计时已清理); }; }, []); // 空依赖effect 只在 mount 时执行cleanup 只在 unmount 时执行 return div查看控制台的倒计时日志。/div; }示例2依赖变化时清理当 userId 发生变化时React 会先调用上一个 effect 返回的 controller.abort()取消旧请求然后执行新的 fetch 请求。当组件卸载前也会执行 controller.abort()避免请求继续运行、然后试图更新已卸载的组件。import React, { useState, useEffect } from react; function FetchData({ userId }) { const [data, setData] useState(null); useEffect(() { const controller new AbortController(); // 发起数据请求 fetch(/api/user/${userId}, { signal: controller.signal }) .then(res res.json()) .then(setData) .catch(err { if (err.name AbortError) { console.log(请求已被取消); } else { console.error(err); } }); // 依赖变化时userId 改变或组件卸载时调用清理函数 return () { controller.abort(); // 取消未完成的请求 }; }, [userId]); // 只有 userId 变动时才重新发起请求 return div用户名{data ? data.name : 加载中...}/div; }⏳ useEffect 中使用异步函数在 React 的函数式组件中我们经常需要在组件挂载或更新时发起异步操作如网络请求、读取本地储存、调用异步 API 等。通常这些副作用逻辑都放在useEffect中。然而useEffect的回调函数本身不能被标记为async因为会返回一个Promise而useEffect期待的是可选的“清理函数”而非Promise。下面从原理、常见写法以及注意事项三个角度详细讲解如何在useEffect中使用异步函数。为什么不能直接把 useEffect 回调写成 asyncfunction MyComponent() { useEffect(async () { // ❌ 这样写是错误的 const res await fetch(/api/data); const data await res.json(); // ... }, []); // ... }返回值的冲突当你给useEffect传入一个async函数时该函数会自动返回一个Promise因为async函数的返回值就是Promise。但 React 要求useEffect回调“要么直接返回undefined要么返回一个同步的清理函数() { ... }”用来在组件卸载或依赖变化时执行清理。若回调返回了PromiseReact 无法识别这段Promise是清理逻辑还是误写因而会抛出警告并导致逻辑混乱。语义不清清理函数cleanup本身必须是同步的负责“撤销”前一次effect创建的资源如取消订阅、清除定时器。如果让useEffect回调变成asyncReact 无法得知你是要在await之前进行清理还是在await之后返回另一个函数这会打乱生命周期的可预测性。因此千万不要将useEffect回调直接写成async。接下来介绍几种常见的正确做法。在 useEffect 里调用异步函数的常见写法在 effect 内部定义并立即执行一个 async 函数这是最常见也最推荐的方式在 useEffect 回调体内先定义一个 async 函数可以用命名函数或箭头函数然后马上调用它。import React, { useEffect, useState } from react; function UserProfile({ userId }) { const [userData, setUserData] useState(null); const [error, setError] useState(null); useEffect(() { // 定义一个 async 函数 async function fetchUser() { try { const response await fetch(/api/user/${userId}); if (!response.ok) { throw new Error(网络错误${response.status}); } const json await response.json(); setUserData(json); } catch (err) { setError(err); } } // 立即执行 fetchUser(); // 可选返回一个清理函数 return () { // 如果你想在 userId 变化时取消之前的请求可以在这里处理 // 例如使用 AbortController 来中止 fetch }; }, [userId]); // 依赖列表中包含 userId当 userId 改变时重新执行上述逻辑 if (error) { return div加载出错{error.message}/div; } if (!userData) { return div加载中.../div; } return div用户名{userData.name}/div; }在useEffect中先定义async function fetchUser()然后同步地调用它。这样useEffect 回调本身依旧是一个同步函数返回值可以是undefined或者一个同步的清理函数。如果需要在组件卸载或依赖变化时取消网络请求可以配合AbortController。使用立即执行的箭头 async 函数IIFE有些人喜欢用 “Immediately Invoked Function Expression”IIFE 的写法更紧凑一些但可读性与上一种等价useEffect(() { (async () { try { const res await fetch(/api/user/${userId}); const data await res.json(); setUserData(data); } catch (err) { setError(err); } })(); // 不返回任何清理函数或者在此处也可返回同步清理 }, [userId]);虽然这种写法更“短”但对一些开发者而言可读性稍低。将异步逻辑抽成自定义 Hook为了让组件逻辑更清晰、可复用我们可以把“异步请求 加载/错误状态管理”封装到自定义 Hook 里然后在组件中直接使用。这样组件本身的 useEffect 只负责“调用 Hook”就好。// useFetch.js import { useState, useEffect } from react; export function useFetch(url) { const [data, setData] useState(null); const [loading, setLoading] useState(true); const [error, setError] useState(null); useEffect(() { const controller new AbortController(); async function fetchData() { setLoading(true); try { const res await fetch(url, { signal: controller.signal }); if (!res.ok) throw new Error(请求失败${res.status}); const json await res.json(); setData(json); } catch (err) { if (err.name ! AbortError) { setError(err); } } finally { setLoading(false); } } fetchData(); // 清理在组件卸载或 url 变更时取消请求 return () { controller.abort(); }; }, [url]); return { data, loading, error }; }组件使用时就非常简洁import React from react; import { useFetch } from ./useFetch; function Dashboard({ userId }) { const { data, loading, error } useFetch(/api/dashboard/${userId}); if (loading) return div加载中.../div; if (error) return div加载出错{error.message}/div; return div欢迎{data.userName}/div; }关注点分离组件只关心要什么数据而不关心“如何发请求并管理状态”。逻辑易复用其他组件也可以复用 useFetch。如果需要在 effect 中做取消逻辑AbortController在一个组件可能在“请求还未返回”时就卸载或者依赖改变需要取消前一次请求时常见做法是借助原生的AbortController配合fetch的signal参数让旧请求可被中止。useEffect(() { const controller new AbortController(); const signal controller.signal; async function loadData() { try { const response await fetch(/api/data/${id}, { signal }); if (!response.ok) throw new Error(错误${response.status}); const json await response.json(); setData(json); } catch (err) { if (err.name AbortError) { // 请求被取消时fetch 会抛出 AbortError console.log(Fetch 已取消); } else { setError(err); } } } loadData(); return () { // 在依赖改变或组件卸载时调用 abort() 取消本次 fetch controller.abort(); }; }, [id]);在effect里先创建AbortController拿到signal。把signal传给fetch一旦执行controller.abort()该fetch就会立刻以AbortError终止。在catch中判断err.name AbortError即可区分“取消”与“真实网络错误”。最后在effect的返回函数里调用controller.abort()实现“组件卸载或id变化时取消当前请求”。多个异步操作与清理如果在同一个useEffect中做多次异步调用例如先拿到token再根据token请求数据可以串联await也要在最外层的effect返回一个总的清理逻辑useEffect(() { const controller new AbortController(); const signal controller.signal; async function loadAll() { try { // 第一步获取 token const tokenRes await fetch(/api/get-token, { signal }); const { token } await tokenRes.json(); // 如果组件在这一步就卸载了tokenRes 会被中止下面 fetch 不会再执行 // 第二步根据 token 请求数据 const dataRes await fetch(/api/data?token${token}, { signal }); const json await dataRes.json(); setData(json); } catch (err) { if (err.name ! AbortError) { setError(err); } } } loadAll(); return () { controller.abort(); }; }, [dependencyA, dependencyB]);使用 Promise.then 也可以如果你不习惯 async/await也可以使用链式 Promise 写法。但同样不能把 useEffect 回调改为异步useEffect(() { const controller new AbortController(); const signal controller.signal; fetch(/api/data/${id}, { signal }) .then(res { if (!res.ok) throw new Error(HTTP 错误${res.status}); return res.json(); }) .then(json { setData(json); }) .catch(err { if (err.name AbortError) { console.log(请求被取消); } else { setError(err); } }); return () { controller.abort(); }; }, [id]);功能与 async/await 等价只是可读性略差。然而在一些需要兼容较旧环境或团队风格偏好 Promise 链时也是常见写法。React 可用useEffect管控组件副作用借助依赖数组和清理函数避免内存泄漏等问题。落地要考虑依赖精准配置、清理逻辑复用的工程效率问题。可试试RollCode 低代码平台它支持私有化部署、自定义组件、静态页面发布SSG SEO。以上就是本文的全部内容啦如果本文对你有帮助的话也可以转发给你的朋友