我们在组件开发中经常需要执行副作用操作:
这是函数式编程里的概念,要彻底理解副作用,首先解释纯函数(Pure function):返回结果只依赖于它的参数,而且没有任何可观察的副作用。函数与外界交流数据只有一个唯一渠道——参数和返回值。
第一点:给纯函数传入相同的参数,永远会返回相同的值。如果返回值依赖外部变量,则不是纯函数。
// 纯函数 不管外部如何天翻地覆,只要传入的参数是确定的,那么值永远是可预料的。
const foo = (a, b) => a + b
foo(1, 2) // => 3
// 非纯函数 返回值也依赖外部变量a,结果无法预料
const a = 1
const foo = (b) => a + b
foo(2)
第二点:一个函数在执行过程中产生了外部可观察的变化,则这个函数是有副作用(Side Effect)的。通俗点就是函数内部做了和运算返回值无关的事,比如修改外部作用域/全局变量、修改传入的参数、发送请求、console.log、手动修改 DOM 都属于副作用。
const foo = (obj, b) => {
obj.x = 2 // 修改了外部变量
return obj.x + b
}
const counter = { x: 1 }
foo(counter, 2) // => 4
counter.x // => 2
纯函数很严格,你几乎除了计算数据什么都不能干,计算的时候还不能依赖自身参数以外的数据。
这个概念拿到 React 中,就是一个 Pure component("纯"组件) 得到相同的 props,永远会渲染出相同的视图,并且没有其他副作用。纯组件的好处是,容易监测数据变化、容易测试、提高渲染性能等。
这里的“纯组件”指的并不是继承自React.PureComponent的class组件——React.PureComponent 和 React.Component的区别是它通过对象浅层对比自动实现shouldComponentUpdate()
,即赋予它相同的state和props不会重复渲染,可以提高性能。但只有在state和props中的数据均结构简单(非引用数据类型)时才适用。
在函数组件中,我们用useEffect Hook代替componentDidMount
,componentDidUpdate
和 componentWillUnmount
来处理副作用操作。
但事实上
useEffect
与 这三个生命钩子函数的执行时机是有区别的。
在layout布局阶段(内存中的真实DOM更新完成后)先异步调度useEffect
中的effect,即按优先级(看下面注释①③)放到异步调度任务队列中,等组件内容被浏览器绘制真正渲染到屏幕之后再按顺序执行这些effect。如果需要再次执行某个useEffect
中的effect,会先执行这个useEffect
return的函数(清除阶段)。组件卸载的时候也会执行useEffect
的清除操作(看②)。和三个钩子执行时机相对贴近的是
useLayoutEffect
(不常用),它内部的effect在真实DOM更新完成后即同步执行,会阻塞浏览器渲染。因此除了手动修改DOM操作之外建议尽量都用useEffect
。useLayoutEffect
return 的函数(destroy函数)时机和componentWillUnmount
(组件从 DOM 中移除时调用)一致,另外组件每次重新渲染也会执行(见②)。
本人非大神,尝试去啃透原理尚觉得吃力,却有一腔追根究底的精神😂,只能实践出真知,先强调几个重要的结论:
-
① 在同一个组件中放置多个useEffect,调度effect的优先级只和声明的顺序有关,组件渲染完毕后就会依次调用它们。如果useEffect有第二个参数(依赖项),当依赖项的值无变化则会跳过这个useEffect。如果想effect仅在组件挂载和卸载时执行,设置依赖项为空数组(
[]
)即可。 React就会知道这个useEffect内的 effect 不依赖于 props 或 state 中的任何值。这个 effect 内部的 props 和 state 一直拥有其初始值。 -
② 在每次需要重新执行某个useEffect的effect前,都会先执行它的清理阶段(useEffect return的函数)。这个时机通常是组件重新渲染后,而不仅仅是在组件卸载的时候(执行组件所有useEffect的清除阶段)。因此对于清除副作用,不要再用组件卸载角度去考虑了,而是对应的清理功能和effect绑定在一个useEffect里,一个useEffect负责处理一个业务模块的思维。
我为测试写了这样几个花里胡哨的组件😂:(代码太长不想贴了,先看个意会)




接下来详细比较下 生命周期函数 和 useEffect 的 区别:
class组件生命钩子时机总结(结合了导航,路由用的是React Navigation 5.x):
- 挂载:添加到DOM; 卸载:从DOM移除
- 初次 navigate 或 push 导航到某个屏幕,触发该屏幕组件的
componentDidMount
(挂载) - 再次 navigate 到一个已经存在于导航栈记录的屏幕,我们回到这个组件的同时导航栈中历史记录在它之后的组件都会卸载(触发
componentWillUnmount
)。而 push 会在添加再次触发这个组件的挂载(因为push代表复用组件,不再是同一个屏幕)。
例如,我们当前有一个历史记录为 Home > Profile > Settings 的导航栈,然后我们调用navigate('Home'),当我们回到Home,意味着剩下的屏幕变成 Home 并删除了 Profile 和 Settings屏幕。 - 按设备回退键,即goBack事件,会触发当前屏幕组件卸载(原理同3.)
- 根据条件动态渲染的时候,触发挂载和卸载,根据渲染与否重复:
componentDidMoun
->componentWillUnmount
- props或state的数据改变重新渲染会触发
componentDidUpdate
函数组件useEffect触发时机总结(同样结合了React Navigation):
-
根据条件动态渲染组件,无论它的useEffect是否有依赖项,每一个都会被触发(根据渲染与否重复以下步骤: A effect -> 清理 A effect )
-
初次渲染,useEffect如果有依赖项,即使父组件没有传该属性,useEffect也会执行,接收到的依赖项的值为undefined
-
若useEffect没有依赖项, 每次组件数据变化都会执行;依赖项为
[]
,只在初次渲染后执行,有依赖项则除了组件初次渲染,只在依赖项改变(引用数据类型只作浅对比)时执行 -
初次 navigate 或 push 导航到某个屏幕,所有useEffect无论是否有依赖项都会被触发(原理同2.)
-
再次 navigate 到一个已经存在于导航栈记录的屏幕,导航栈中历史记录在它之后的组件都会被删除, 对应触发这些已卸载组件所有useEffect的清除阶段。
-
而 push 会再次触发这个组件的所有useEffect(同2.) 。为复用组件,不再是同一个屏幕。
-
按设备回退键,即goBack事件(因为goBack也是删除在目标屏幕历史记录之后的所有屏幕),会触发当前屏幕组件所有useEffect的清除阶段。(原理同5.)
-
props或state的数据改变重新渲染组件时会触发无依赖项或依赖项为改变数据的useEffect。
class组件
挂载和更新函数是分开的,因此当需要执行和清理一些副作用,经常要在 componentDidUpdate 钩子重新写一遍,副作用和清理函数也要分别写在挂载和卸载钩子里。而useEffect
让我们只需专注于组件是否(重新)渲染了,不论是它是初次挂载还是更新。不仅实现逻辑复用,将副作用与它相应的清理函数也牢牢挂钩,代码结构更清晰合理。
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) {
// 取消订阅之前的 friend.id
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// 订阅新的 friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
useEffect 的清理阶段执行时机,可以再根据官方演示吃透一下:
// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // 运行第一个 effect
// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // 运行下一个 effect
// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // 运行下一个 effect
// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一个 effect
网友评论