为何useEffect清理函数在首次渲染时执行?非卸载场景解析
useEffect清理函数首次渲染后触发的原因解析
组件代码(SideMenu)
import React, { useContext, useRef } from "react"; import useMenu from "../Hooks/useMenu"; import { Context } from "../context/SideMenuContext"; import { StyledSideMenu } from "./StyledComponent/StyledSideMenu"; import Button from "./Button"; import useOnClickOutside from "../Hooks/useOnClickOutside"; import Hamburger from "./Hamburger"; function SideMenu() { const { IsOpen, setIsOpen } = useContext(Context); const wrapperref = useRef(); const items = useMenu(); useOnClickOutside(wrapperref, () => { setIsOpen(false); }); return ( <StyledSideMenu ref={wrapperref} IsOpen={IsOpen}> <Hamburger /> {items} <Button padding="5% 10%">Resume</Button> </StyledSideMenu> ); } export default SideMenu;
Hook代码(useOnClickOutside)
import { useEffect } from "react"; function useOnClickOutside(ref, handler) { useEffect(() => { const listener = (event) => { if (!ref.current || ref.current.contains(event.target)) { return; } handler(); }; document.addEventListener("mousedown", listener); document.addEventListener("touchstart", listener); return () => { document.removeEventListener("mousedown", listener); document.removeEventListener("touchstart", listener); console.log("cleaned up"); }; }, [ref, handler]); } export default useOnClickOutside;
问题
为何useEffect的清理函数会在首次渲染后执行?此时侧边菜单组件并未从DOM卸载,甚至从未卸载,仅宽度变为0。已知清理函数通常在组件卸载时执行,且应用未包裹StrictMode,需了解该行为背后的React原理。
原理解析
首先要明确React中useEffect的核心执行规则:
- 组件挂载时,执行副作用函数;
- 当
useEffect依赖项数组中的值发生引用变化时,会先执行上一次副作用的清理函数,再执行新的副作用函数; - 组件卸载时,执行最后一次副作用的清理函数。
回到你的代码,清理函数触发的核心原因是依赖项的引用变化导致useEffect重新执行:
- 每次
SideMenu组件重新渲染(比如IsOpen状态变化时,组件会因为props/state更新而重新渲染),你传给useOnClickOutside的handler参数是一个全新的箭头函数:() => {setIsOpen(false);}。因为JavaScript中每次创建的函数都是独立的引用,所以每次渲染时这个handler的引用都不一样。 - 第一次渲染时,
useEffect执行,添加了mousedown和touchstart事件监听,同时保存了对应的清理函数。 - 当
IsOpen变化触发组件重新渲染后,useOnClickOutside的依赖项handler发生了引用变化,React会先调用上一次副作用的清理函数(也就是你看到的console.log("cleaned up")输出),然后再执行新的副作用函数,添加新的事件监听。 - 至于
ref参数,useRef创建的wrapperref本身的引用是固定不变的(只有ref.current会变化),所以它不是导致依赖项变化的原因。
解决办法
如果不想让清理函数在组件重新渲染时触发,可以用useCallback钩子固定handler的引用:
// 在SideMenu组件中修改handler的定义 const closeMenu = React.useCallback(() => { setIsOpen(false); }, [setIsOpen]); // setIsOpen是useState返回的函数,引用固定,所以依赖项可以安全传入 useOnClickOutside(wrapperref, closeMenu);
这样closeMenu的引用会被缓存,只有当依赖项setIsOpen变化时才会重新创建(实际上setIsOpen的引用永远不会变),此时useEffect的依赖项不再变化,清理函数只会在组件卸载时执行。
内容的提问来源于stack exchange,提问作者Mohiul Islam
相关产品推荐
相关产品推荐

