React和jQuery事件处理程序以错误的顺序触发

时间:2014-09-16 07:00:33

标签: javascript-events reactjs

我正在创建显示在屏幕顶部的菜单。当用户点击其中一个菜单项div时会出现很多链接。我不能通过点击另一个菜单项(没问题,已经实现它)来隐藏这个div,而且还可以点击其他任何地方然后点击这个div和menuitems。

我听说过两个解决方案:

  • 显示菜单后面的隐藏div并覆盖整个屏幕 并为其添加点击处理程序。
  • 将事件处理程序附加到document.body。

第一个对我不好,因为div将覆盖页面上的链接,我希望它们可以点击,即使在点击menuitem后它出现了相应的div。所以我尝试了第二次灵魂。但问题是在我的组件的处理程序之前触发了body上的jquery click处理程序。我不知道如何让它首先调用我的组件处理程序,然后阻止事件传播。

这是代码和js小提琴:

/** @jsx React.DOM */
var Menu = React.createClass({
    click: function(e) {
        console.log('component handled click - should be called before jquery one and prevent jquery handler from running at all');
        e.stopPropagation();
    },
    render: function(){
    console.log("component renders");
        return (
            <div>
                <div>| MenuItem1 | MenuItem2 | MenuItem 3 |</div>
                <br />
                <div className="drop">
                    MenuItem 1 (This div appears when you click menu item)
                    <ul>
                        <li><a href="#" onClick={this.click}>Item 1 - when I click this I don't want document.body.click to fire</a></li>
                        <li><a href="#" onClick={this.click}>Item 1 - when I click this I don't want document.body.click to fire</a></li>
                    </ul>
                </div>
            </div>
        );
    }
});

React.renderComponent(<Menu />, document.getElementById("menu"));

$(document).ready(function() {
    console.log('document is ready');
    $("body").click(function() {
        console.log('jquery handled click - should not happen before component handled click');
    });
});

http://jsfiddle.net/psonsx6j/

启动控制台,然后运行它以解释问题

2 个答案:

答案 0 :(得分:15)

所以我解决了。 使用jQuery向文档添加事件处理程序时,将首先触发此事件处理程序。所以你无法捕捉事件并阻止它的传播。但是当你使用document.addEventlistener附加处理程序时,这个处理程序最后被调用,你有机会反应:)点击React组件。

我修改了Mike的代码以使其有效(http://jsfiddle.net/s8hmstzz/):

/** @jsx React.DOM */
var Menu = React.createClass({
    getInitialState: function() {
        return {
            show: true
        }
    },
    componentDidMount: function() {
        // when attaching with jquery this event handler is run before other handlers
        //$('body').bind('click', this.bodyClickHandler);
        document.addEventListener('click', this.bodyClickHandler);
    },
    componentWillUnmount: function() {
        //$('body').unbind('click', this.bodyClickHandler);
        document.removeEventListener('click', this.bodyClickHandler);
    },
    anchorClickHandler: function(e) {
        console.log('click on anchor - doing some stuff and then stopping propation');
        e.stopPropagation();
        e.nativeEvent.stopImmediatePropagation();
    },
    bodyClickHandler: function(e) {
        console.log('bodyclickhandler');
        this.setState({show: false})
    },
    clickHandler: function(e) {
        // react to click on menu item
    },
    preventEventBubbling: function(e) {
        console.log('click on div - stopping propagation');
        e.stopPropagation();
        e.nativeEvent.stopImmediatePropagation();
    },
    render: function() {
        return (
            <div>
                <a href="#" onClick={this.anchorClickHandler}>Link - will stop propagation</a>
                <div id="menudrop" onClick={this.preventEventBubbling} style={{display: this.state.show ? 'block' : 'none'}}>
                    <ul>
                        <li onClick={this.clickHandler}>Menu Item</li>
                    </ul>
                </div>
            </div>
        )
    }
});

React.renderComponent(<Menu />, document.getElementById('menu'));

启动控制台,然后单击div,anchor和body以查看其工作原理。 我不知道为什么使用addEventListener而不是jQuery更改事件处理程序的触发顺序。

答案 1 :(得分:8)

请参阅上面的评论。

基本上,当结合使用jquery(或普通的JS事件)时,你不能依赖能够在React中停止传播像onClick这样的事件。

您可以做的是检查您的$('body').click()功能,而不是单击菜单(因此单击其中的项目后不会关闭菜单)。另外值得注意的是,你应该在反应组件中绑定这个主体事件,这样你就可以通过安装组件来干净地添加/删除它,而不会让组件本身不知道的任何东西显着改变组件的功能。

所以你做的是这样的事情(nb:未经测试的代码,但是基于我对同一问题的组件)

var Menu = React.createClass({
    getInitialState: function() {
        return {
            show: true
        }
    },
    componentDidMount: function() {
        $('body').bind('click', this.bodyClickHandler);
    },
    componentWillUnmount: function() {
        $('body').unbind('click', this.bodyClickHandler);
    },
    bodyClickHandler: function(e) {
        if ($(e.target).parents('div').get(0) !== this.getDOMNode()) {
            this.setState({
                show: false
            })
        }
    },
    clickHandler: function(e) {
        // react to click on menu item
    },
    render: function() {
        return (
            <div style={{display: this.state.show ? 'block' : 'none'}}>
                <ul>
                    <li onClick={this.clickHandler}>Menu Item</li>
                </ul>
            </div>
        )
    }
});

因此,在上面的代码中,我们假设您希望在组件安装到dom后立即显示菜单下拉列表。这可能不是这种情况,因为您可能希望安装组件,然后根据悬停显示/隐藏它。在这种情况下,您只需要将$('body').bind()事件绑定从componentDidMount移动到回调,只要您显示菜单。每当隐藏组件时,$('body').unbind()都应该移动到。然后你会得到一个完美的情况,当菜单显示时,我们绑定一个等待点击的身体事件,并在它被隐藏和/或卸载时取消绑定。

当点击菜单时,bodyClickHandler之前调用clickHandler会发生什么,但bodyClickHandler仅在您点击的父树不包含根div时隐藏菜单this.getDOMNode() 1}}我们的组件(render()给出<li>返回的根节点的html元素。

因此,在组件外部单击将关闭它,单击组件内部将不执行任何操作,单击clickHandler将触发{{1}}。一切都按预期工作。

希望这有帮助。