博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
react hooks api 踩坑经历(useEffect的闭包陷阱和useRef)
阅读量:6206 次
发布时间:2019-06-21

本文共 4491 字,大约阅读时间需要 14 分钟。

笔者最近用时下正新鲜的hooks api重构了自己的项目,其中踩到了一些坑在此与大家分享一下。

考虑这样的场景:网页内容分为数页(每页fixed定位),鼠标滚轮触发翻页,类似ppt的感觉,我们用currPage这个状态来记录当前页码,然后在componentDidMount中挂上滚轮事件。class实现的代码如下:

class App extends React.Component{  state = {    currentPage: 0,  }  /** @description 用于防抖的实例私有字段 */  flipSwitch = true;  filpPage = (event) => {    if (this.flipSwitch) {      if(event.deltaY > 0){        this.setState({currentPage: (this.state.currentPage + 1) % 3});      }      if(event.deltaY < 0){        if(this.state.currentPage > 0){          this.setState({currentPage: this.state.currentPage - 1});        }      }      this.flipSwitch = false;      setTimeout(() => {this.flipSwitch = true;}, 1000);    }  }  componentDidMount(){    window.addEventListener('wheel', this.filpPage);  }  componentWillUnmount(){    window.removeEventListener('wheel', this.filpPage);  }    render(){    const classNames = ['unvisible','unvisible','unvisible'];    classNames[this.state.currentPage] = 'currPage';    return (      
) }}复制代码

这里的重构用到两个hook:useState,useEffect。useState相信大家都了解了(不了解的戳这里:),这里我说一说useEffect的用法:首先它对标的是class中的各个生命周期,接收的第一个参数是一个回调函数effectCallback,这个回调是这样的形式:() => (void | (() => void | undefined)),如果useEffect没有第二个参数,effectCallback会在每次render(或者组件函数被调用)后被调用,相当于componentDidMount+componentDidupdate,值得注意的是effectCallback往往还会return一个函数,它的角色类似componentWillUnmount,会在下一次render前或销毁组件前运行。那么这里我只需要'像componentDidMount那样在组件第一次render后运行一次而不是每次render后都运行'要如何实现呢?这就需要传给useEffect第二个参数-一个数组了,它的角色有点类似于componentShouldUpdate,一般来说它由state和props中的数据构成,每次render,useEffect会判断这个数组与上一次render是否完全一致,如果完全一致effectCallback就不会进入事件队列并运行了,想要实现componentDidmount的效果只需传一个空数组,这样数组每次都完全一致了。

此外还有一个问题:函数防抖的变量flipSwitch放哪儿呢?答案是useRef。从命名上看它对标的是ref,也就是用来在render中收集dom元素,不过官方文档上已经说明了它的作用不止于此:**However, useRef() is useful for more than the ref attribute. It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.**它的使用方法与useState没太大的差别,唯一要注意的是它不会直接把值return给你,而是以{current:value}这样的形式。 here is code:

function App () {  const [currPage, setCurrPage] = useState(0)  const classNames = Array(pageCount).fill('unvisible');  const flipSwitch = useRef(true)  classNames[currPage] = 'currPage';  useEffect(() => {    console.log('add listenner!')    const wheelListener = event => {      if (flipSwitch.current) {        if(event.deltaY > 0){          setCurrPage((currPage + 1) % pageCount);        }        if(event.deltaY < 0){          if(currPage > 0){            setCurrPage(currPage - 1)          }        }        flipSwitch.current = false;        setTimeout(() => {flipSwitch.current = true;}, 1000);      }    };    window.addEventListener('wheel', wheelListener);    // 在下次更新 listener 前运行return的函数    return () => {console.log('rm listenner!'),window.removeEventListener('wheel', wheelListener)}  },[]);  return (    
)}复制代码

重点来了,程序一跑就会发现翻页只能从第一页翻到第二页。ok,来看看哪里出了问题。wheelListener每次滚轮都有触发,那么问题很可能出在currPage这个变量上了,果然,通过打印currPage我们会发现wheelListener每次触发它的值都一样是0。

原来wheelListener中除了event以外的变量都是第一次render中声明函数时通过闭包得到的,自然除了event以外的值一直都是第一次render时的值。

知道了原因,接下来有两种可能的解决方式:

  1. 设法在wheelListener取到currPage即时的值,因为没有了class语法中的this引用这条路看来是走不通了
  2. 每次render后更新wheelListener并删除旧的。

这同时也解决了我之前的一个疑惑:'每次render都会声明一个新的回调函数给useEffect,这是不是很浪费?',看来这很可能就是官方设计api时设想好的用法,给useEffect第二个参数也省了。

那么,把useEffect的第二个参数去掉,或者为了更贴合使用场景把它设置为[currPage],现在它可以正常运行了:

function App () {  const [currPage, setCurrPage] = useState(0)  const classNames = Array(3).fill('unvisible');  const flipSwitch = useRef(true)  classNames[currPage] = 'currPage';  useEffect(() => {    console.log('add listenner!')    const wheelListener = event => {      if (flipSwitch.current) {        if(event.deltaY > 0){          setCurrPage((currPage + 1) % 3);        }        if(event.deltaY < 0){          if(currPage > 0){            setCurrPage(currPage - 1)          }        }        flipSwitch.current = false;        setTimeout(() => {flipSwitch.current = true;}, 1000);      }    };    window.addEventListener('wheel', wheelListener);    // 在下次更新 callback 前运行return的函数    return () => {console.log('rm listenner!'),window.removeEventListener('wheel', wheelListener)}  }, [currPage]);  return (    
)}复制代码

PS: hooks api 还是很香的:

  1. 函数式组件看起来清爽多了。
  2. 对比class形式 render-vm-state 的结构,hooks api 把结构变成了更简洁的 render-states, 其中的state,react还帮你代劳了,你只需给它初始值即可。
  3. 自定义hooks可以复用一些在class语法下难以复用的逻辑(一般是因为this指向问题)。
  4. 函数式组件结合函数式编程有了更多的想象空间(结合compose、柯里化什么的)。

转载地址:http://sokja.baihongyu.com/

你可能感兴趣的文章
数据类型之Integer与int
查看>>
转载:ASP.NET在后台代码实现个功能,根据选择提示用户是否继续执行操作
查看>>
静态代理设计与动态代理设计
查看>>
uva-10152-乌龟排序
查看>>
ThreadLocal源码剖析
查看>>
每天一个linux命令(12):more命令
查看>>
奈奎斯特采样定理:
查看>>
Java笔试之Singleton
查看>>
android自动化框架简要剖析(一):运行原理+基本框架
查看>>
处理测试环境硬盘爆满
查看>>
Python函数积累
查看>>
jq挑战30天——打字机效果+小程序
查看>>
正则表达式怎样匹配 不包含特定字符串的字符串
查看>>
bzoj 2296: 【POJ Challenge】随机种子
查看>>
MPU和MCU的区别和选择
查看>>
js call
查看>>
apply和call用法
查看>>
学习笔记之-------UIScrollView 基本用法 代理使用
查看>>
如何理解运维
查看>>
Dom学习笔记
查看>>