更深入地理解 React 事件冒泡/传播和状态管理

Deeper understanding of React event bubbling / propagation and state management

如果这是一个愚蠢的问题,我提前道歉。虽然我已经设法让它工作了,但我想更深入地了解它。

我正在 React 中构建一个自定义汉堡菜单,只要您单击无序列表或汉堡图标本身以外的任何地方,该菜单就会关闭。

我在这里看到了答案 我已经关注了它,但我不明白为什么它不起作用。

首先,当它只是汉堡包图标并且没有点击菜单外的关闭选项时,它工作得很好。 然后,当我使用 useRef 挂钩获取对无序列表的引用以便仅在未单击列表时关闭菜单时,它工作得很好,除了当我单击实际的汉堡包图标时。

经过大量的业余调试,我终于意识到发生了什么。 首先,当我打开菜单时,状态 showMenu 变为 true, 然后当我点击汉堡包图标关闭时, parent 包装元素首先触发而不是汉堡包菜单,这很奇怪,因为在冒泡阶段我希望最里面的元素首先触发。

因此 parent 元素将关闭菜单更改状态,导致组件 re-render。然后,当事件到达实际图标时,handleClick 将再次将状态切换为 true,给人的印象是汉堡包点击不起作用。

我通过在 parent 元素上使用 event.stopPropogation() 设法解决了这个问题。 但这看起来很奇怪,因为我不希望 parent 元素的点击首先触发,尤其是当我使用冒泡阶段时。 我唯一能想到的是因为它是一个原生 dom addeventlistener 事件,它在合成事件之前首先触发。

下面是带有汉堡包的移动导航代码 header 组件根据屏幕宽度呈现普通 Nav 或 MobileNav。我没有把高阶组件的代码放到更容易通过,但如果需要我可以提供所有代码:

//MobileNav.js
export default function MobileNav() {

    const [showMenu, setShowMenu] = useState(false);
    const ulRef = useRef();

    console.log('State when Mobilenav renders: ', showMenu);

    useEffect(() => {
        
        let handleMenuClick = (event) => {

            console.log('App Clicked!');

            if(ulRef.current && !ulRef.current.contains(event.target)){
                setShowMenu(false);
                event.stopPropagation();
            }
        }

        document.querySelector('#App').addEventListener('click', handleMenuClick);

        return () => {
            document.querySelector('#App').removeEventListener('click', handleMenuClick);
        }
    }, [])

    return (
        <StyledMobileNav>
            <PersonOutlineIcon />
            <MenuIcon showMenu={showMenu} setShowMenu={setShowMenu} />

            {
                (showMenu) &&
                    <ul ref={ulRef} style={{
                            backgroundColor: 'green',
                            opacity: '0.7',
                            position: 'absolute',
                            top: 0,
                            right: 0,
                            padding: '4em 1em 1em 1em',
                        }}
                    >

                        <MenuList/>
                    </ul>
            }

            
        </StyledMobileNav>
    )
}

//MenuIcon.js

/**
* By putting the normal span instead of the MenuLine component after > worked in order to hover all div's
*/
const MenuWrap = styled.div`
    width: 28px;
    position: relative;
    transform: ${(props) => props.showMenu ? `rotate(-180deg)` : `none` };
    transition: transform 0.2s ease;
    z-index: 2;

    &:hover > div{
        background-color: white;
    }
`;

const MenuLine = styled.div`
    width: 100%;
    height: 2px;
    position: relative;
    transition: transform 0.2s ease;
    background-color: ${(props) => props.showMenu ? 'white' : mainBlue};
    &:hover {
        background-color: white;
    }
`;

const TopLine = styled(MenuLine)`
    ${(props) => {
        let style = `margin-bottom: 7px;`;
        if(props.showMenu){
            style += `top: 9px; transform: rotate(45deg);`;
        }
        return style;
    }}
`;

const MidLine = styled(MenuLine)`
    ${(props) => {
        let style = `margin-bottom: 7px;`;
        if(props.showMenu){
            style += `opacity: 0;`;
        }
        return style;
    }}
`;

const BottomLine = styled(MenuLine)`
    ${props => {
        if(props.showMenu){
           return `bottom: 9px; transform: rotate(-45deg);`;
        }
    }}
`;

export default function MenuIcon({showMenu, setShowMenu}) {

    const handleMenuClick = (event) => {
        console.log('Menu Clicked!');
        console.log('State before change Icon: ', showMenu);
        setShowMenu(!showMenu);
    }

    return (
        <MenuWrap onClick={handleMenuClick} showMenu={showMenu}>
            <TopLine onClick={handleMenuClick} showMenu={showMenu}></TopLine>
            <MidLine onClick={handleMenuClick} showMenu={showMenu}></MidLine>
            <BottomLine onClick={handleMenuClick} showMenu={showMenu}></BottomLine>
        </MenuWrap>
    )
}

阅读这篇文章 https://dev.to/eladtzemach/event-capturing-and-bubbling-in-react-2ffg 基本上它指出 React 中的事件与 DOM 事件的工作方式基本相同

但是由于某些原因事件冒泡不能正常工作 请参阅下面的屏幕截图,其中显示了状态如何变化:

谁能解释为什么会这样或出了什么问题?

这是竞争事件侦听器的常见问题。看来你已经解决了问题是点击关闭处理和菜单按钮点击关闭处理同时触发并相互抵消。

根据 DOM3 规范,事件侦听器应该按照它们附加的顺序被调用,但是旧的浏览器可能不实现这个规范(参见这个问题:The Order of Multiple Event Listeners)。在您的情况下,点击监听器(在 <MobileNav> 组件中)首先附加(因为您在那里使用 addEventListener,而 child 使用 React onClick 道具)。

与其依赖事件侦听器的添加顺序(这可能会变得棘手),不如更新您的代码,以便触发不会同时发生(这是此答案概述的方法)或者这样处理程序中的逻辑就不会重叠。

解决方法:

如果将 ref'd 元素向上移动一个级别,使其同时包含菜单按钮和菜单本身,则可以避免 overlapping/competing 事件。

这样,菜单按钮位于 space 内,点击会被忽略,因此在单击菜单按钮时不会触发外部点击侦听器(点击退出侦听器),但如果用户点击菜单或其按钮以外的任何地方。

例如:

return (
    <StyledMobileNav>
        <PersonOutlineIcon />
        <div ref={menuRef}>
            <MenuIcon showMenu={showMenu} setShowMenu={setShowMenu} />
            { showMenu && (
                <ul>
                    <MenuList/>
                </ul>
            )}
        </div>
    </StyledMobileNav>
)

然后使用 menuRef 作为检查外部点击的方法。

作为附加建议,尝试将所有菜单逻辑放入一个组件中以便更好地组织,例如:

function Menu() {
    const [showMenu, setShowMenu] = React.useState(false);

    // click out handling here
    
    return (
        <div ref={menuRef}>
            <MenuIcon showMenu={showMenu} setShowMenu={setShowMenu} />
            { showMenu && (
                <ul>
                    <MenuList/>
                </ul>
            )}
        </div>
    )
}