记录一次React程序死循环
一、错误复现
开发环境报如下错误。
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
Call Stack
checkForNestedUpdates
website/./node_modules/react-dom/cjs/react-dom.development.js:4013:321
scheduleUpdateOnFiber
website/./node_modules/react-dom/cjs/react-dom.development.js:3606:187
dispatchAction
website/./node_modules/react-dom/cjs/react-dom.development.js:2689:115
eval
website/./src/components/FileUpload.jsx:73:7
invokePassiveEffectCreate
website/./node_modules/react-dom/cjs/react-dom.development.js:3960:1047
HTMLUnknownElement.callCallback
website/./node_modules/react-dom/cjs/react-dom.development.js:657:119
Object.invokeGuardedCallbackDev
website/./node_modules/react-dom/cjs/react-dom.development.js:677:45
invokeGuardedCallback
website/./node_modules/react-dom/cjs/react-dom.development.js:696:126
flushPassiveEffectsImpl
website/./node_modules/react-dom/cjs/react-dom.development.js:3968:212
unstable_runWithPriority
website/./node_modules/scheduler/cjs/scheduler.development.js:465:16
二、错误排查
- 通过注释代码的方式,发现出问题的地方,是
Assets
组件中引用的FileUpload
出了问题。正好最近也修改过FileUpload
组件。 - 通过sourcetree对比git记录,看
FileUpload
组件被修改了什么?如下图。 - 再对比错误提示中的描述,其中
componentWillUpdate or componentDidUpdate
,推测就是指新增的useEffect
代码片断。 - 将上述
useEffect
代码片断注释掉,果然错误消失。
三、原因分析
useEffect
的特性表明,只要initFiles
发生了改变,46-48行代码就会执行。
既然上述useEffect
代码片断事实上造成了死循环,就还说明了一点:
setFileList(initFiles)
改变了initFiles
,才使得useEffect
中的函数再次被调用。
那么,initFiles
到底是经历了怎样的变化,才使得调用能够循环往复地发生呢?
输出fileList
和initFiles
:
console.log(fileList === initFiles)
可以发现,只有第一次render时输出true
,后续全部是false
。
- 第一次输出
true
,表明useState
的入参为array
时,只是简单的赋值关系,fileList
和initFiles
指定了同一个内存地址。 setFileList
函数实际上是做了一次浅拷贝,然后赋值给fileList
,改变了fileList
的内存指向,也就是改变了最新initFiles
的内存指向。同时React保留了之前initFiles
的值用来做依赖对比。useEffect
在对比引用类型的依赖,比如object/array
时,采用的是简单的===
操作符,也就是说比较内存地址是否一致。- 前后两次
initFiles
虽然内部数据相同,但内存指向不同,就被useEffect
认为【依赖发生了改变】,从而导致了死循环。
四、解决方案1
尽量不直接使用object或者array作为依赖项,而是使用值类型来代替引用类型
useEffect(() => {
//...
}, [initFiles.length])
五、解决方案2
是不是在调用useState
时,拷贝initFiles
就可以了呢?
const [fileList, setFileList] = useState([...initFiles])
useEffect(() => {
if (fileList.length === 0) {
setFileList([...initFiles])
}
}, [initFiles])
这样依然会报同样的死循环错误,这又是为什么呢?
initFiles
是从父组件传入的,会不会是FileUpload组件重新render的时候,initFiles
已经被重新赋值了呢?接下来的两个demo,证明了这个推测。
- Demo1 - 慎重打开。打开后会卡死浏览器标签: initFiles初始化时,使用
[]
作为默认值,结果出现死循环。 - Demo1 - 放心打开。打开后不执行JS,不会卡死浏览器,可放心查看代码。
- Demo2:initFiles初始化时,不使用默认值,且父组件不更新,结果不出现死循五。
Demo1中,initFiles
作为一个prop
,每次render时,都会被赋值为一个新的空数组,改变了其内存指向。导致useEffect
不断执行。
const FileUpload = ({initFiles=[]}) => {}
Demo2中,initFiles
的值完全由父组件传入,父组件的变量不变化时,initFiles
没有改变。
const FileUpload = ({initFiles=[]}) => {}
const App = () => {
return <FileUpload initFiles={[]} />
}
也就是说,只要保障initFiles
不被循环赋值,就能够避免死循环。
六、结论
不建议将引用类型如array/object
作为useEffect
的依赖项,因欺触发bug的可能性很大,而且排查错误比较困难。
建议使用一到多个值类型作为useEffect
依赖项。