一文搞定 React 的 useEffect
当依赖为引用数据类型时
依赖是引用数据类型和基本类型对比
每次点击发现只会输出 useEffect - obj
,也就是当 useEffect 依赖为对象时,每次更新都会执行。
依赖是如何进行比较的
1 |
|
比较分为三种方式:
- 当 prevDeps 为 null ,也就是没传入依赖(
在这之前会对依赖进行处理,如果传入依赖为 undefined 会赋值 null
),始终返回 false,每次都执行 - 如果传入依赖就进行遍历,通过 Object.is 进行比较,当比较出不等时返回 false,也就是依赖发生变化时执行
- 遍历结束认为依赖没发生变化,返回 true,不执行
解决方案
上文分析了问题产生的原因,那么应该如何处理呢?
使用 useRef
因为 ref 会单独开辟一个地址,在组件更新前后不会新建,这也是能够用 useRef 进行缓存的原因。这里我们使用 useRef 包裹一个对象,每次对比的都是相同的对象,所以 useEffect 只会在初始化执行,更新时不会执行。
useEffect 返回函数 cleanup
执行时机
cleanup 会在组件销毁、依赖改变的情况下执行。
当改变 num 值和控制 Child 组件隐藏销毁组件时,都会有输出。
清除副作用
cleanup 函数通常用来清除副作用,例如清理定时器,清除事件监听等,这里用计时器作为例子:
这段代码中我们就需要每次清空循环定时器才能达到想要的效果,不然每次更新都会创建循环定时器。
useEffect 模拟声明周期
React hooks 提供 useEffect、useLayoutEffect 来解决函数式组件没有声明周期的问题。
componentDidMount
1 |
|
当 useEffect 依赖为 []
时,setup 函数也就是 useEffect 的第一个参数,只在组件初始化时执行。
componentWillUnmount
1 |
|
在上一个的基础上,setup 函数的返回函数 cleanup 函数会在每次依赖项变更重新渲染后,用旧的值执行一遍,当组件从页面卸载也会执行一遍。一般在这里清除定时器,关闭事件监听等等。
componentWillReceiveProps
1 |
|
将 props 放在 useEffect 的依赖中,会在 props 变化时执行。和 componentWillReceiveProps 区别在于 useEffect 在 commit 阶段才会执行,componentWillReceiveProps 在 render 阶段就执行,另外 useEffect 在初始化时默认执行一次。
componentDidUpdate
1 |
|
不设置 useEffect 的依赖,将会在组件状态更新时执行。和 componentDidUpdate 区别在于 useEffect 是异步执行,componentDidUpdate 是同步执行,并且 useEffect 在初始化时默认执行一次。
无限循环
当我们模拟 componentDidUpdate 时,如果在 setup 函数中更新状态,会导致 更新状态 -> 执行 -> 更新状态 -> 执行 ...
无限循环执行。
useEffect、useLayoutEffect、useInsertionEffect
useEffect 在浏览器渲染完成之后异步执行,useLayoutEffect 在 DOM 更新后,浏览器渲染前同步执行,会阻塞浏览器绘制。对于 DOM 的操作一般放在 useLayoutEffect 中,因为 useEffect 执行时 DOM 已经渲染完成了,再去修改 DOM 可能会产生回流或者重绘。
在 React18
版本又提供 useInsertionEffect 来处理 CSS-in-JS 在渲染中注入样式的性能问题。