useEffect()主要用于管理副作用,例如通过网页抓取、直接操作DOM、启动和结束计时器。虽然useEffect()和useState(管理状态的方法)是最常用的钩子,但需要一些时间才能熟悉并正确使用它们。使用useEffect()时可能遇到的一个陷阱是组件渲染的无限循环。在本文中,我们将探讨创建无限循环的常见场景以及如何避免它们。1.死循环和副作用更新状态假设我们有一个功能组件,里面有一个输入元素,该组件的功能是统计输入变化的次数。我们将这个组件命名为CountInputChanges,大致内容如下:functionCountInputChanges(){const[value,setValue]=useState('');const[count,setCount]=useState(-1);useEffect(()=>setCount(count+1));constonChange=({target})=>setValue(target.value);返回(
)}
是受控组件。value变量保存输入输入的值,当用户输入输入时,onChange事件处理程序更新值状态。这里使用useEffect()来更新计数变量。useEffect(()=>setCount(count+1))每次组件因用户输入而重新渲染时都会更新计数器。因为使用useEffect(()=>setCount(count+1))时没有依赖参数,所以()=>setCount(count+1)会在组件每次渲染后执行回调。你觉得这样写会有问题吗?打开demo自己试试:https://codesandbox.io/s/infinite-loop-9rb8c?file=/src/App.js运行你会发现count状态变量是不受控制的自增,甚至不用输入任何东西在输入中,这是一个无限循环。问题在于useEffect()的使用方式:useEffect(()=>setCount(count+1));它会生成组件重新渲染的无限循环。初始渲染后,useEffect()执行更新状态的副作用回调函数。状态更新触发重新渲染。重新渲染后,useEffect()执行副作用回调并再次更新状态,这将再次触发重新渲染。1.1可以通过正确管理useEffect(callback,dependencies)依赖参数来解决具有依赖关系的无限循环。由于我们希望计数在值更改时递增,因此我们可以简单地使值成为副作用的依赖项。import{useEffect,useState}from'react';functionCountInputChanges(){const[value,setValue]=useState('');const[count,setCount]=useState(-1);useEffect(()=>setCount(计数+1),[value]);constonChange=({target})=>setValue(target.value);return(
);}添加[value]作为useEffect的依赖,这样只有当[value]发生变化时,count状态变量才会更新。这样做可以解决无限循环。1.2使用ref除了依赖,我们还可以使用useRef()来解决这个问题。这个想法是更新Ref不会触发组件的重新渲染。import{useEffect,useState,useRef}from"react";functionCountInputChanges(){const[value,setValue]=useState("");constcountRef=useRef(0);useEffect(()=>countRef.current++);constonChange=({target})=>setValue(target.value);return(
Numberofchanges:{countRef.current}
);}useEffect(()=>countRef.current++)countRef.current++会在每次因为变化重新渲染值时返回。引用更改本身不会触发组件重新渲染。2.无限循环和新对象引用即使正确设置了useEffect()依赖项,在使用对象作为依赖项时也要小心。例如下面的组件CountSecrets,在input中监听用户输入的单词。一旦用户输入特殊词“secret”,“secret”的计数将增加1。import{useEffect,useState}from"react";functionCountSecrets(){const[secret,setSecret]=useState({value:"",countSecrets:0});useEffect(()=>{if(secret.value==='秘密'){setSecret(s=>({...s,countSecrets:s.countSecrets+1}));}},[秘密]);constonChange=({target})=>{setSecret(s=>({...s,value:target.value}));};return(
秘密数量:{secret.countSecrets}
);}打开演示(https://codesandbox.io/s/infinite-loop-obj-dependency-7t26v?file=/src/App.js)和自己试试试试,当前输入secret,secret.countSecrets的值开始不受控制的增长。这是一个无限循环问题。为什么会这样?秘密对象用作useEffect(...,[secret])。在副作用回调函数中,只要输入值等于secret,就会调用更新函数setSecret(s=>({...s,countSecrets:s.countSecrets+1}));这会增加countSecrets的值,但也会创建一个新对象。秘密现在是一个新对象,并且依赖关系已更改。所以useEffect(...,[secret])被再次调用,副作用是更新状态并再次创建一个新的秘密对象,等等。JavaScript中的两个对象只有在引用完全相同的对象时才相等。2.1避免使用对象作为依赖项解决循环创建新对象造成的无限循环问题的最好方法是避免在useEffect()的依赖项参数中使用对象引用。letcount=0;useEffect(()=>{//somelogic},[count]);//好!letmyObject={prop:'Value'};useEffect(()=>{//somelogic},[myObject]);//不好!useEffect(()=>{//somelogic},[myObject.prop]);//好!修复组件的死循环问题,可以将useEffect(...,[secret]))改为foruseEffect(...,[secret.value])。只需在secret.value更改时调用副作用回调就足够了,这里是固定代码:import{useEffect,useState}from"react";functionCountSecrets(){const[secret,setSecret]=useState({value:"",countSecrets:0});useEffect(()=>{if(secret.value==='secret'){setSecret(s=>({...s,countSecrets:s.countSecrets+1}));}},[secret.value]);constonChange=({target})=>{setSecret(s=>({...s,value:target.value}));};return(
Numberofsecrets:{secret.countSecrets}
);}3SummaryuseEffect(callback,deps)在组件中渲染后执行回调(副作用)的钩子。如果不注意副作用是如何工作的,就会触发组件渲染的无限循环。生成无限循环的常见情况是在没有指定任何依赖参数的情况下更新副作用状态useEffect(()=>{//Infiniteloop!setState(count+1);});避免无限循环的有效方法是正确设置Dependencies:useEffect(()=>{//NoinfiniteloopsetState(count+1);},[whenToUpdateValue]);另外,也可以使用Ref,更新Ref不会触发重新渲染:useEffect(()=>{//NoinfiniteloopcountRef.current++;});无限循环的另一种常见方法是使用一个对象作为useEffect()的依赖项,并在副作用中更新该对象(有效地创建一个新对象)useEffect(()=>{//Infiniteloop!setObject({...object,prop:'newValue'})},[object]);避免使用对象作为依赖项,只使用特定的属性(最终结果应该是原始值):useEffect(()=>{//NoinfiniteloopsetObject({...object,prop:'newValue'})},[object.whenToUpdateProp]);在使用useEffect()的时候,你知道还有什么方法可以造成死循环陷阱吗?~完了,我是小智,下次见~作者:Shadeed译者:前端小智