useEffect大部分场景都是用于异步地setState,或者是异步地读取ref,或者是进行订阅事件。但是由于react setState会引发组件rerender,所以需要设置正确的deps避免重复执行。
deps比较的原理,就是利用了useRef去记录上一次的deps,然后做比较。
简化版:
const useEffect = (create: Effect, deps?: readonly any[]) => { const fiber = getCurrentWorkInProgress() const effect = fiber.memoizedState // useRef去记录上一次的deps const prevDepsRef = useRef(null) if (deps && areHookInputsEqual(deps, prevDepsRef.current)) { return } else { prevDepsRef.current = deps fiber.memoizedState = pushEffect(effect, create) } } // 比较函数 const areHookInputsEqual = ( nextDeps: readonly any[], prevDeps: readonly any[] | null ) => { if (prevDeps === null) { return false } for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) { // is函数简单来说就是 a === b if (is(nextDeps[i], prevDeps[i])) { continue } return false } return true }
注意这个deps的比较是浅比较。
deps设置注意点与useEffect引起的死循环
deps是浅比较,所以deps数组中的元素最好是基本类型,也就是以值作比较。如果deps中包含了对象,那么比较的是对象的引用地址,并不会深比较(react为了性能认为没有必要去深比较),而且在react函数组件中,对象经常会因为setState引起的rerender而重新初始化导致引用变化,造成useEffect重复执行,如果此时useEffect中又进行了setState则又会导致deps变化而引起死循环无限setState。
会引起死循环的useEffect示例:
const [state, setState] = useState(0) // 由于setState使组件rerender,obj将重新初始化,引用发生变化 const obj = { id: 0 } useEffect(() => { setState(state + 1) }, [obj])
如果一定要用对象做deps,必须使用useMemo或者useState包裹obj对象:
const obj = useMemo(() => ({ id: 0 }), []) // const [obj] = useState({ id: 0 }) const [state, setState] = useState(0) useEffect(() => { setState(state + 1) }, [obj]) // useEffect(() => { // setState(state + 1) // }, [obj.id])
特别是从props上传来的obj,更无法知道它是不是已经经过useMemo优化,所以建议deps最好是基本类型数组!例如可以用obj.id代替obj做deps。