记录一些刚写react踩过的坑吧,想到多少就写多少
从一个工作中的需求开始
有个需求是,点击按钮出来一个弹窗。本来弹窗有个关闭按钮,但是UED在视觉稿上面加了行字,“点击关闭按钮或弹窗外的其他地方,关闭弹窗”。我想了想,合理,避免有些奇怪的操作使得弹窗没关掉。实现的方法就是监听点击事件呗,然后判断点击的是哪,如果弹窗是打开的,并且点击的地方不在弹窗里,就把弹窗关掉。
具体代码我直接CSDN找了一个,好用的。通过createref来获取dom,对document使用addEventListener,监听点击的event.target,判断和弹窗dom的关系就可以了。本来是个挺简单的事,但是我用了之后发现,本来那个打开弹窗的按钮也不好用了。后来发现,因为那个按钮也是弹窗之外,所以弹窗打开就马上关闭了。一开始没想到事件捕获和冒泡,我想当然地以为,应该先触发document的点击事件,这个时候弹窗没出现,所以无事发生,然后再触发按钮的点击事件,打开弹窗。这和实际的结果并不相符。
我调试的时候发现,点击按钮后总是先打开弹窗,再触发document的点击事件,把弹窗关上。我当时想的解决办法是,把点击按钮的onClick外面包一层setTimeout,延迟0。将打开按钮的操作改成了异步事件,根据事件循环机制,异步会在同步代码执行完毕一轮之后,再去执行,这样就保证了打开弹窗的操作,一定在判断点击位置然后关闭弹窗之后。这样就能正常打开弹窗了。
其他的解决方法
后来我下班之后,又继续研究这个问题,这时候才想明白,之前两个点击事件的执行顺序不对,是因为事件的捕获和冒泡机制。原来onClick和addEventListener默认都是冒泡,所以执行顺序就是先最低层级的dom元素,最后是document,就出问题了。然而addEventListener的第三个参数改成true,就变成捕获阶段了,这样就对了。然后我就想,写个简单的demo来验证一下。
问题并不简单
然后就出问题了,我用vite脚手架建了个基本的react demo,然后用函数式组件,直接在函数体内部(没有使用useEffect)完成了对document添加点击响应事件的操作,然后我发现,怎么每回一点击屏幕,都会触发两次点击事件。我寻思为啥会触发两次呢?我顿时就想到,有可能是添加了两个点击响应事件。如果像我这样直接在函数体内部,不使用useEffect把addEventListener添加到一个元素上,然后组件重新渲染,函数重新执行,就会对一个事件添加多个响应函数。当然本身或者子组件除外,因为重新渲染的时候点击事件也没了。
函数式编程与副作用
但是我这显然是没有重新渲染的,一个连state都没有的根组件咋可能会去重新渲染。但是我通过chrome的api,getEventListeners查看,发现确实有2个eventListener。然后我试着加了个state,然后改变state,发现eventListener变成4个了,再改变state,变成6个了。这更加说明了每次组件重新渲染的时候,都渲染了两次。后来我去查了一些,发现是react18的严格模式下,开发环境的时候,组件的渲染会执行两遍,就是为了让开发者发现不应该出现的副作用。就比如我的这个添加eventListener应该写在useEffect内部,并且通过useEffect的return,清除副作用。这也给我提了个醒。
函数式组件写起来很方便,很简便。但是和类组件不同,类组件重新渲染只会重新执行render函数,而函数式组件是通篇执行。所以函数式组件更要注意副作用,一定要写在useEffect里面,并且做好清除工作,比如remove响应事件。然后我感觉,我对函数式编程和副作用的理解还有待加强。只能说这一个小需求,让我收获了很多。虽然后来mentor跟我说这种不影响整体功能的需求没必要花时间,但是我认为这次我的尝试,还是让我对react有了更深一步的了解。