从一个 bug 讲起
之前在一次需求中,需要通过监听全局事件,处理相关逻辑,于是自然的想到这样写:
function Page() {
const [lists, setLists] = useState([])
// 在首次渲染后获取数据、设置状态
useEffect(() => {
get('/api').then(res => setLists(res))
// 监听登录成功事件,处理对应业务逻辑
event.on('login_success', handleLoginSuccess)
return () => {
event.off('login_success', handleLoginSuccess)
}
}, [])
function handleLoginSuccess() {
console.info('lists: ', lists)
}
return <div>{lists.map(list => <List key={list.id} data={list} />)}</div>
}
复制代码
大家能看出上面的代码哪里有问题吗??
问题在于 handleLoginSuccess
方法中获取到的 lists 永远是空数组 ?
我们的想法很单纯,组件首次渲染后为监听事件设置回调函数,在其被触发时读取当前状态;
但 useEffect
表示你一点也不懂我:
不过别慌,问题不大,useEffect
的内心世界很详细地介绍在 React 官方文档 和 Dan 的博客文章 里
下面用我自己的理解带大家过一遍:
useEffect 的使用指南
useEffect 盲猜就知道是和 effect 副作用相关的 hook
“副作用” 笼统地讲,是指会更改函数参数或外部作用域变量,每次调用可能产生不同结果的操作,与 纯函数
的概念相反
所以 数据获取、修改 DOM 元素、设置监听事件 等就很适合放在 useEffect
中执行
useEffect(() => {
getDataFromApi(res => setState(res))
getElementById('root').scrollTo(100)
document.body.addEventListener('scroll', handlePageScroll)
})
复制代码
同时,通过 useEffect
第二个参数 – > 依赖数组,让 React 可以通过比对数组元素,知道什么时候需要重新执行 effects:
// 在 title 或 count 更新时都会触发 effect 执行 ?
useEffect(() => {
document.title = `${title} - ${count}`
}, [title, count])
复制代码
最后,React 还贴心地提供了清除函数让我们在组件卸载前进行一些扫尾工作:
useEffect(() => {
window.addEventListener('scroll', handlePageScroll)
return (() => {
// 将在组件卸载前执行
window.removeEventListener('scroll', handlePageScroll)
})
}, [])
复制代码
Hook 的出现让我们可以用函数组件替代 class 组件,其中 useEffect
就承担着模拟生命周期的重任
让我们结合上面三个特性模拟一下:
// 模拟 componentDidMount 和 componentWillUnMount
useEffect(() => {
// 处理组件首次渲染后相关逻辑
// 假装这里有代码
return () => {
// 处理组件卸载前相关逻辑
}
}, [])
/**
* 模拟 componentDidUpdate
* 不加依赖数组是比较危险的,组件每次重新渲染后都会执行 effects 方法,负担较重
* 同时一不小心还会导致无限循环
* 这样写之前一定要三思哦
*/
useEffect(() => {
// 处理状态更新后相关逻辑
console.info('props: ', props)
console.info('count state: ', count)
})
复制代码
滴滴滴,打卡收工~ 你已经学完了 useEffect
的全部用法,因为我只知道这些
下面了解一下 useEffect
的注意事项吧 ?
useEffect 的注意事项
useEffect
的执行时机
useEffect
是在页面渲染后开始执行,不会阻塞浏览器渲染,这一点与 componentDidMount
不同;
另外组件树中 useEffect
执行顺序是由内向外:
function ChildOne() {
useEffect(() => {
console.info('Child One get rendered.')
}, [])
return (
<div>
Child One
<GrandSon/>
</div>
)
}
function ChildTwo() {
useEffect(() => {
console.info('Child Two get rendered.')
}, [])
return <div>Child Two</div>
}
function GrandSon() {
useEffect(() => {
console.info('GrandSon get rendered.')
}, [])
return <div> GrandSon</div>
}
function App() {
useEffect(() => {
console.info('App get rendered.')
}, [])
return (
<div>
<ChildOne/>
<ChildTwo/>
</div>
)
}
复制代码
大家可以猜下 log 打印顺序 ?
—- 剧透预警 —-
- 每次渲染时
useEffect
只能获取那次 特定渲染 时的状态值
这也是开篇举例的 bug 来源,简单来说是介样子:
function ShowCount() {
const [count, setCount] = useState(0)
useEffect(() => {
setTimeout(() => console.info('count: ', count), 3000)
}, [])
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
复制代码
它的执行结果:
在我将 count 增加到 6 后,useEffect 设置的定时器才缓缓地吐出 ”count:0“
能理解我第一次遇到这个 bug 时绝望的心情吗 ?
问题的原因是这里的 effect 只会在首次渲染后执行一次,此后会一直保留获取的初始状态值
现在修复它就是小 case 了,我们需要在 count
更新后重新运行下 effect 方法 :
function ShowCount() {
const [count, setCount] = useState
const timerRef = useRef(null)
useEffect(() => {
timerRef.current = setTimeout(() => console.info('count: ', count), 3000)
return () => {
clearTimeout(timerRef.current)
}
}, [count]) // 在 count 变化时重新执行 effect 方法,确保获取最新状态值 ✅
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
复制代码
切记 effects 里用到的状态值(props && state)都要尽量放进依赖数组里
- 对待依赖数组要认真负责
从上面的缺陷大家应该认识到依赖数组的重要性了
所以如果你想在 effects 里获取最新状态值,就不要在依赖数组上对 React 撒谎,不然受伤的总会是你 ?
useEffect(() => {
document.title = `${title} - ${count}`
}, [title, count]) // 在 title 或 count 更新后及时运行 effect 方法 ✅
复制代码
但像 setState
等触发状态更新的函数,是不需要放入依赖数组滴,因为这些方法是稳定不变的,并且它们是触发状态更新,不是获取状态:
useEffect(() => {
getContentFromApi().then(res => setContent(res))
}, []) // ✅
复制代码
嗯,山寨一个低配版 useEffect
最后搓搓小手实现一个简单的 useEffect
?
let _deps = undefined
function useEffect(callback, dependencies) {
const hasChanged = _deps && dependencies.some((dep, i) => dep !== _deps[i])
// 如果没有依赖数组或依赖元素相对上一次有变化,则执行回调且更新 依赖数组 记录,方便下一次比较
if (!dependencies || hasChanged) {
callback()
_deps = dependencies
}
}
复制代码
从上面简单实现能再次体会到依赖数组的重要性,一定不能大意哦 ~
以上是自己对 useEffect
的小小总结,才疏学浅,如有纰漏,还望各位大佬不吝指出