初识 useEffect Hook

从一个 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 表示你一点也不懂我:

not_understand.gif
不过别慌,问题不大,useEffect 的内心世界很详细地介绍在 React 官方文档Dan 的博客文章

下面用我自己的理解带大家过一遍:

zb_mode.jpeg

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 的注意事项

  1. 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>&nbsp;&nbsp;&nbsp;&nbsp;GrandSon</div>
    }
  
    function App() {
      useEffect(() => {
        console.info('App get rendered.')
      }, [])
  
      return (
        <div>
          <ChildOne/>
          <ChildTwo/>
        </div>
      )
    }
复制代码

大家可以猜下 log 打印顺序 ?

—- 剧透预警 —-

image.png

  1. 每次渲染时 useEffect 只能获取那次 特定渲染 时的状态值

这也是开篇举例的 bug 来源,简单来说是介样子:

    function ShowCount() {
        const [count, setCount] = useState(0)
        
        useEffect(() => {
            setTimeout(() => console.info('count: ', count), 3000)
        }, [])
        
        return <button onClick={() => setCount(count + 1)}>{count}</button>
    }
复制代码

它的执行结果:

image.png
在我将 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)都要尽量放进依赖数组里

image.png

  1. 对待依赖数组要认真负责

从上面的缺陷大家应该认识到依赖数组的重要性了
所以如果你想在 effects 里获取最新状态值,就不要在依赖数组上对 React 撒谎,不然受伤的总会是你 ?

    useEffect(() => {
        document.title = `${title} - ${count}`
    }, [title, count]) // 在 title 或 count 更新后及时运行 effect 方法 ✅
复制代码

但像 setState 等触发状态更新的函数,是不需要放入依赖数组滴,因为这些方法是稳定不变的,并且它们是触发状态更新,不是获取状态:

image.png

    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 的小小总结,才疏学浅,如有纰漏,还望各位大佬不吝指出

heart.jpeg

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享