前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用 React useEffect 的一个小坑

使用 React useEffect 的一个小坑

作者头像
皮小蛋
发布2020-03-02 10:31:35
1.4K0
发布2020-03-02 10:31:35
举报
文章被收录于专栏:前端皮小蛋前端皮小蛋

今天讲一个 使用 useEffect Hooks 的时候遇到的一个小陷阱,看下面的代码。

一个Counter,在窗口大小改变的时候,输出当前count:

function App() {
  const [count, setCount] = useState(0)

  useEffect(() => {
     window.addEventListener('resize', handleResize)
     return () => window.removeEventListener('resize', handleResize)
  }, [])

  const handleResize = () => {
    console.log(`count is ${count}`)
  }

  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}> + </button>
      <h1>{count}</h1>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Counter:

现在我们如果点击 + 按钮,下面的数字0会加1 .

这时候你去改变浏览器窗口的大小,console上会输出什么呢?

你可能觉得是1 :

count is 1

但是事实上,输出是这样:

count is 0

怎么会这样呢?

我先直接说这个问题怎么修复吧。

关键在useEffect是用法上,正确的写法是这样:

  useEffect(() => {、
     window.addEventListener('resize', handleResize)
     return window.removeEventListener('resize', handleResize)
  }, [count]) // 加 Count

看了fix之后你也许就知道这是怎么回事了。

useEffect的第二个参数可选,如果用上的话,这个参数必须是一个数组。

useEffect 在每次被调用的时候,都会“记住”这个数组参数,当下一次被调用的时候,会逐个比较数组中的元素,看是否和上一次调用的数组元素一模一样,如果一模一样,第一个参数(那个函数参数)也就不用被调用了,如果不一样,就调用那个第一个参数。

当我们代码中的App组件第一次被渲染的时候,useEffect百分之百会调用第一个函数参数,这时候count变量是0,但是,当我们点+按钮让Counter增长为1,这时候App被重新渲染,但是因为useEffect第一个参数总是一个空数组,所以不会重新做addEventListener的工作。

你可能又会问:就算useEffect不重新执行第一个函数参数,也不应该有什么问题啊,handleResize函数利用闭包(clousre)功能访问App中的count变量,那也应该是使用更新为1的count啊!

虽然闭包的确可以访问外围的变量,但是,此handleResize非彼handleResize。

第一次渲染时的handleResize和第二次渲染时的handleResize,虽然源自同一段代码,但是在运行时却是两个不同的函数对象。这并不难理解,handleResize是一个局部变量,每次App被执行时,handleResize都会被重行赋值,所以每一次App被渲染时,都会给handleResize一个全新的函数对象。

复盘一下:

  1. App第一次被渲染
    1. 给handleResize赋值了一个函数对象(我们姑且用XX-1代表),这个XX-1引用的count值是这一次App被渲染时的count值,值为0;
    2. handleResize被useEffect挂到resize事件上,以后,当resize时间发生时,handleResize(应该说是XX-1)被调用;
  2. App第二次被渲染
    1. 有一次给handleResize赋值了一个函数对象,代号YY-2,注意,这个YY-2和之前的XX-1不是同一个函数对象,XX-1依然引用的是值为0的count,但是YY-2引用的是值为1的count;
    2. handleResize(也就是YY-2)没有被useEffect挂到resize时间上,换句话说,YY-2这个函数对象压根没有派上用场。
  3. resize事件发生了
    1. window上挂的resize事件处理,是第一次渲染时候创造的XX-1号handleResize,所以方位的count值为0

希望现在你明白了。

总结一下,要明白这几点:

  • React Hooks只能用于函数组件,而每一次函数组件被渲染,都是一个全新的开始。
  • 每一个全新的开始,所有的局部变量全都重来。
  • 每一次全新的开始,只有Hooks函数(比如useEffect)才具有上一次渲染的“记忆”;

对于上面说的问题,因为count每次渲染都会改变,而且我们想要 useEffect 总会用上count的值,所以,就要把count放在useEffect的第二个数组参数里面。

如果useEffect第一个函数参数直接或者间接用上某个变量,就请把这个变量放在useEffect的第二个参数里。

如果根本不用useEffect的第二个参数呢?

也行,但是,这样每次渲染都会执行useEffect的第一个参数,这……在某些场景下有一点性能浪费。

其实要做到上面的规矩,也没那么难,不过在实际操作的时候,的确让人容易失误,你看,在上面的例子中,useEffect并没有直接使用count,只不过使用了handleResize,handleResize虽然直接使用了count,但是它作为一个独立函数并不知道(或者说也不该知道)自己会被useEffect用到,这……让人防不胜防啊!

这只有一层简介调用,假设useEffect调用了函数X,函数X调用了Y,Y调用了Z。

调用N层之后再调用 handleResize,真的不容易看出useEffect需要加上对count的依赖。

所以,使用useEffect的时候,不要调用函数层次太多,代码应该一眼看清楚哪些函数会被useEffect调用。

最后, eslint-plugin-react-hooks 插件可以给出依赖提示, 一定程度上能避免类似的问题。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-10-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端皮小蛋 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档