React Hooks的依赖项

3,138 阅读3分钟

有感这篇文章👉函数式编程看React Hooks(二)事件绑定副作用深度剖析,作者写得条理很清晰。

在此再强调一遍: 为什么要用useCallback去缓存onMouseMove这个函数?因为该函数是一个useEffect的依赖项,而且它是个引用类型的值,所以每一次re-render都会是新的函数。倘若该函数未发生变化,其实没有必要每次re-render都生成新的,这也是useCallbak最主要的作用。

下面延伸一丢丢,还是讲hooks的依赖项。从我自己还原的部分HashRouter代码讲起:

export default function HashRouter(props) {
    let locationState = null;

    const [location, setlocation] = useState({
        pathname: window.location.hash.slice(1) || "/",
        state: locationState
    });

    const handleHashChange = useCallback(() => {
        console.log('执行 useCallback里面的函数') // 只要hashchange就会执行
        setlocation({
            ...location,
            pathname: window.location.hash.slice(1), // 新pathname的值并没有依赖上一个pathname
            state: locationState
        })
    }, []); // 这里并没有添加依赖

    useEffect(() => {
        console.log('执行useEffect函数') // 只会打印一次
        window.addEventListener('hashchange', handleHashChange)
        return () => {
            window.removeEventListener('hashchange', handleHashChange)
        }
    }, []); // 这里也没有添加依赖

    const val = {
        location,
        history: {
                push: to => {
                    if (typeof to === 'object') {
                        let {
                            pathname,
                            state
                        } = to;
                        window.location.hash = pathname;
                        locationState = state;
                        
                    } else {
                        window.location.hash = to;
                    }
            }
        }
    }
    return <ReactRouterContext.Provider value={val}>
        {
            props.children
        }
    </ReactRouterContext.Provider>
}

请看代码中的注释部分。是的,我在上述useCallback和useEffect中都没有添加依赖项。其实这是不好的,会造成疑惑和不解。

但另一方面,它实现了缓存函数&&只绑定一次hashchange事件&&在hashchange时正常切换页面的设计初衷:

  • 实现缓存handleHashChange这个方法。它只在初次render的 时候生成,之后re-render都返回的是缓存的函数。为什么让依赖项是空这么设计呢?因为它确实没有需要的依赖项。handleHashChange内部的setlocation方法的值,都不依赖useState返回的变量。浏览器事件的特殊之处就在于,只要不卸载页面或者手动解绑,绑定之后就一定会触发。
  • useEffect虽然只执行一次,但是只需要在初次执行的时候给hashchange绑定函数即可,不必每次re-render都重新去绑定和解绑。

当我一开始对比文章开头引用的文章和自己这段代码时,也感到疑惑,再仔细去筛查,发现:在setlocation时,是从外部获取新值的这个原因促成的。所以才能够实现依赖项为空,且照样能刷新的目的。

下面我们引入一组对照组,就可以发现确实如此:

export default function HashRouter(props) {
    let locationState = null;

    const [location, setlocation] = useState({
        pathname: window.location.hash.slice(1) || "/",
        state: locationState
    });
    const [count, setcount] = useState({  // +++++ 新增这一段 +++++
        number: 0
    })

    const handleHashChange = useCallback(() => {
        setcount({
            number: count.number+1  // +++++ 新增这一段 +++++
        });
        setlocation({
            ...location,
            pathname: window.location.hash.slice(1),
            state: locationState
        })
    }, []);

    useEffect(() => {
        window.addEventListener('hashchange', handleHashChange)
        return () => {
            window.removeEventListener('hashchange', handleHashChange)
        }
    }, []);
    console.log('count--------', count);  // +++++ 新增这一段 +++++ //会发现count只会增加到1,然后就不变了。因为 handleHashChange的依赖项是空。依赖项放置count就可以实现count递增了。
    console.log('location', location);   // +++++ 新增这一段 +++++
    // 这个之所以会变化,是因为它的计算,是从window.location.hash中设置新值的,而不是依赖前一个值
   
    const val = {
        location,
        history: {
                push: to => {
                    if (typeof to === 'object') {
                        let {
                            pathname,
                            state
                        } = to;
                        window.location.hash = pathname;
                        locationState = state;
                        
                    } else {
                        window.location.hash = to;
                    }
            }
        }
    }
    return <ReactRouterContext.Provider value={val}>
        {
            props.children
        }
    </ReactRouterContext.Provider>
}

再看看执行这段代码的打印结果:

这个结果就和上面推荐的文章的结果相同了。