javascript React ·

使用react-hooks在事件监听中state不更新问题

在使用react开发网站时,使用事件监听是常有的事情,但是有时候你会发现一个问题,就是这个state有时候不更新,始终是一个值,让人很是费解。下面我们来看一下下面这个案例

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

    useEffect(()=>{
        document.addEventListener('scroll', scrollEventListener)
        return ()=>{
            document.removeEventListener('scroll', scrollEventListener)
        }
    },[]);

    const onClick = () => {
        console.log("click:"+count);
        setCount(count++)
    }

    let scrollEventListener = ()=>{

        console.log("linstener"+count)
    }

    return (<button onClick={onClick}> {count} </button>);
}

我们这个页面是一个长长的页面,是有滚动条的,当你点击按钮时,会依次打印出count自增前的值,但是当你滚动页面时,你会发现这个count始终是1,无论怎么点击都不变,让人很好奇,为什么click事件可以拿到最新的count值,但是监听事件中拿不到呢?

经过多番查找,终于找到了原因--闭包

原理

其实我们所使用的函数组件在本质上就是执行一个函数后返回的组件,在之前的文章中有讲过关于闭包和作用域链的问题,在此不再赘述,这里重点说一下在组件中是如何形成闭包

当这个组件第一次渲染时,App函数会被执行,此时生成生成作用域对象obj {count: 1, setCount, onClick}。

关键点是在于useEffect,这个useEffect形成了一个闭包,而且其中的闭包只在App组件第一次渲染的时候执行,

这个闭包的外部作用域就是上面的obj对象。在这个闭包内的滚动监听事件中,所获得的count值显然是从外围作用域对象obj上找到的, 而obj的count属性是const修饰的,它不可能在App内发生改变的,因此打印的始终是1(这就是我们经常出现异常的地方,发现count没能更新)。

点击按钮,调用setCount触发App组件重新渲染,App函数会重新执行,此时通过useState拿到最新的count值为2。生成新的作用域对象obj2 {count: 2, setCount, onClick},因此打印的 outer count = 2。

App重新渲染时,useEffect内的闭包并不会执行,监听事件中拿到的count始终是第一次App执行的时候生成的作用域对象的count属性值1, 拿不到最新的count值。

怎么解决闭包拿不到最新的count值,其中一个解决方案用到了useEffect的第二个参数,这个参数发生变化时会执行最新的闭包。

 useEffect(()=>{
        document.addEventListener('scroll', scrollEventListener)
        return ()=>{
            document.removeEventListener('scroll', scrollEventListener)
        }
    },[count]);

但是个人不建议这么做,因为如果是其依赖的数据过多,最造成频繁增加监听事件和解除监听事件,所产生的性能开销会很大,还有另外一个办法可以实现,就是通过useEffect监听相关的state变量,来执行具体的业务,如下:

useEffect(()=>{
       console.log(count)
    },[count]);

这个例子比较简单,通常情况下遇到多种变量,我们可以在监听事件中使用setCount,对于count变化后具体的执行放在useEffect中即可。

另一种state不生效的场景

另一中state不生效的场景其本质也是闭包,也是由于useEffect的第二个参数为[]引起的,不知道大家遇到过没有,个人初次遇到时很是懵逼。

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


    const onClick = () => {
        console.log("click:"+count);
        setCount(count++)
    }

    let scrollEventListener = ()=>{

        console.log("linstener"+count)
    }

    return (<Bcd onClick={onClick} />);
}
funciton Bcd(props){

    useEffect(()=>{
        document.getElementById("#cc").addEventListener('click', () => {
         props.onClick();
        })

    },[]);

    return <Button id="cc"></Button>

}

我这是举了一个简单的例子,实际情况是在子组件当中使用了一个编辑器,需要在初次生成组件时生成编辑器对象,而且只在初次时生成,内部需要在内容修改是调用父组件的onChange事件,为了简化使用上面的例子也能看出效果。

从上面的例子中我们可以发现执行后count也是不会发生变化的,其根本原因也是在于useEffect的闭包,解决方案和签名相同,在这里说一下只是想提醒大家在遇到此类问题时一脸懵逼。

参与评论