如何使用React和鼠标事件传播实现可重用的组件?

时间:2018-05-11 18:43:30

标签: javascript reactjs redux race-condition event-propagation

考虑以下典型的React文档结构:

Component.jsx

<OuterClickableArea>
    <InnerClickableArea>
        content
    </InnerClickableArea>
</OuterClickableArea>

这些组件的组成如下:

OuterClickableArea.js

export default class OuterClickableArea extends React.Component {
    constructor(props) {
        super(props)

        this.state = {
            clicking: false
        }

        this.onMouseDown = this.onMouseDown.bind(this)
        this.onMouseUp = this.onMouseUp.bind(this)
    }

    onMouseDown() {
        if (!this.state.clicking) {
            this.setState({ clicking: true })
        }
    }

    onMouseUp() {
        if (this.state.clicking) {
            this.setState({ clicking: false })
            this.props.onClick && this.props.onClick.apply(this, arguments)
            console.log('outer area click')
        }
    }

    render() {
        return (
            <div
                onMouseDown={this.onMouseDown}
                onMouseUp={this.onMouseUp}
            >
                { this.props.children }
            </div>
        )
    }
}

为了这个参数,除了类名和控制台日志语句之外,InnerClickableArea.js具有几乎相同的代码。

现在,如果您要运行此应用程序并且用户单击内部可点击区域,则会发生以下情况(如预期):

  1. 外部区域注册鼠标事件处理程序
  2. 内部区域注册鼠标事件处理程序
  3. 用户在内部区域按下鼠标
  4. 内部区域的mouseDown侦听器触发器
  5. 外部区域的mouseDown侦听器触发器
  6. 用户释放鼠标
  7. 内部区域的mouseUp侦听器触发器
  8. 控制台日志&#34;内部区域点击&#34;
  9. 外部区域的mouseUp侦听器触发器
  10. 控制台日志&#34;外部区域点击&#34;
  11. 这显示了典型的事件冒泡/传播 - 到目前为止没有任何意外。

    将输出:

    inner area click
    outer area click
    

    现在,如果我们创建的应用程序一次只能进行一次交互,该怎么办?例如,想象一下编辑器,可以通过鼠标点击选择元素。当在内部区域内按下时,我们只想选择内部元素。

    简单明了的解决方案是在InnerArea组件中添加一个stopPropagation:

    InnerClickableArea.js

        onMouseDown(e) {
            if (!this.state.clicking) {
                e.stopPropagation()
                this.setState({ clicking: true })
            }
        }
    

    这可以按预期工作。它将输出:

    inner area click
    

    问题?

    InnerClickableArea特此隐含地选择OuterClickableArea(和所有其他家长)不能接收活动。即使InnerClickableArea不应该知道OuterClickableArea的存在。就像OuterClickableArea不了解InnerClickableArea一样(在关注点和可重用性概念分离之后)。我们在OuterClickableArea&#34;知道&#34;的两个组件之间创建了一个隐式依赖关系。它不会错误地让听众被解雇,因为它会记住&#34; InnerClickableArea将停止任何事件传播。这似乎不对。

    我尝试不使用stopPropagation来提高应用程序的可扩展性,因为添加依赖事件的新功能会更容易,而不必记住哪些组件在哪个时间使用stopPropagation。 / p>

    相反,我想使上述逻辑更具说明性,例如:通过在Component.jsx内以声明方式定义每个区域当前是否可点击。我会让应用状态知道,传递区域是否可点击,并将其重命名为Container.jsx。像这样:

    Container.jsx

    <OuterClickableArea
        clickable={!this.state.interactionBusy}
        onClicking={this.props.setInteractionBusy.bind(this, true)} />
    
        <InnerClickableArea
            clickable={!this.state.interactionBusy}
            onClicking={this.props.setInteractionBusy.bind(this, true)} />
    
                content
    
        </InnerClickableArea>
    
    </OuterClickableArea>
    

    其中this.props.setInteractionBusy是一个redux操作,会导致应用状态更新。此外,此容器将app状态映射到其props(未在上面显示),因此将定义this.state.ineractionBusy

    OuterClickableArea.js (对于InnerClickableArea.js几乎相同)

    export default class OuterClickableArea extends React.Component {
        constructor(props) {
            super(props)
    
            this.state = {
                clicking: false
            }
    
            this.onMouseDown = this.onMouseDown.bind(this)
            this.onMouseUp = this.onMouseUp.bind(this)
        }
    
        onMouseDown() {
            if (this.props.clickable && !this.state.clicking) {
                this.setState({ clicking: true })
                this.props.onClicking && this.props.onClicking.apply(this, arguments)
            }
        }
    
        onMouseUp() {
            if (this.state.clicking) {
                this.setState({ clicking: false })
                this.props.onClick && this.props.onClick.apply(this, arguments)
                console.log('outer area click')
            }
        }
    
        render() {
            return (
                <div
                    onMouseDown={this.onMouseDown}
                    onMouseUp={this.onMouseUp}
                >
                    { this.props.children }
                </div>
            )
        }
    }
    

    问题是Javascript事件循环似乎按以下顺序运行这些操作:

    1. 创建OuterClickableAreaInnerClickableArea,其中道具clickable等于true(因为app state interactionBusy默认为false)
    2. 外部区域注册鼠标事件处理程序
    3. 内部区域注册鼠标事件处理程序
    4. 用户在内部区域按下鼠标
    5. 内部区域的mouseDown侦听器触发器
    6. 点击内部区域
    7. 容器运行&#39; setInteractionBusy&#39;动作
    8. redux app状态interactionBusy设置为true
    9. 外部区域的mouseDown侦听器触发器(clickable道具仍为true,因为即使应用状态interactionBusytrue,也会尚未导致重新渲染;渲染操作放在Javascript事件循环结束时)
    10. 用户释放鼠标
    11. 内部区域的mouseUp侦听器触发器
    12. 控制台日志&#34;内部区域点击&#34;
    13. 外部区域的mouseUp侦听器触发器
    14. 控制台日志&#34;外部区域点击&#34;
    15. 对重新呈现Component.jsx进行反应,并将clickable作为false传递给两个组件,但到现在为止已经太晚了
    16. 这将输出:

      inner area click
      outer area click
      

      而期望的输出是:

      inner area click
      
      因此,应用程序状态无助于尝试使这些组件更加独立和可重用。原因似乎是即使状态更新,容器也只在事件循环结束时重新渲染,并且鼠标事件传播触发器已在事件循环中排队。

      因此,我的问题是:是否有替代方法可以使用app状态以声明方式处理鼠标事件传播,或者是否有更好的方法来实现/执行我的上面用app(redux)状态设置?

4 个答案:

答案 0 :(得分:2)

最简单的替代方法是在触发stopPropogation之前添加一个标志,在这种情况下标志是一个参数。

const onMouseDown = (stopPropagation) => {
stopPropagation && event.stopPropagation();
}

现在,甚至应用程序状态或道具甚至可以决定触发停止传播

<div onMouseDown={this.onMouseDown(this.state.stopPropagation)} onMouseUp={this.onMouseUp(this.props.stopPropagation)} >
    <InnerComponent stopPropagation = {true}>
</div>

答案 1 :(得分:1)

不是处理组件中的click事件和clickable状态,而是使用LOCK状态到Redux状态,当用户单击时,组件只发出CLICK动作,保持锁定并做逻辑。像这样:

  1. 创建了OuterClickableArea和InnerClickableArea
  2. Redux状态,字段CLICK_LOCK默认为false
  3. 外区注册鼠标事件处理程序
  4. 内部区域注册鼠标事件处理程序
  5. 用户点击内部区域
  6. 内部区域的mouseDown侦听器触发
  7. 内部区域发出操作INNER_MOUSEDOWN
  8. 外区mouseDown侦听器触发
  9. 外区发出行动OUTER_MOUSEDOWN
  10. INNER_MOUSEDOWN操作将CLICK_LOCK状态设置为true并执行逻辑(日志inner area click)。
  11. 由于CLICK_LOCKtrueOUTER_MOUSEDOWN不执行任何操作。
  12. 内部区域的mouseUp侦听器触发
  13. 内部区域发出操作INNER_MOUSEUP
  14. 外区mouseUp侦听器触发
  15. 外区发出行动OUTER_MOUSEUP
  16. INNER_MOUSEUP操作将CLICK_LOCK州设置为false
  17. OUTER_MOUSEUP操作将CLICK_LOCK州设置为false
  18. 除非点击逻辑改变某些状态,否则无需重新渲染。
  19. 注意:

    • 这种方法通过在区域中锁定,同时只有一个组件可以click。每个组件都不必了解其他组件,但需要一个全局状态(Redux)来协调。
    • 如果点击率很高,这种方法的表现可能不会很好。
    • 目前Redux操作为guaranteed to be emitted synchronously。不确定未来。

答案 2 :(得分:1)

我怀疑你是要创建像叠加模式这样的东西,当点击外部组件或容器组件时,应该关闭组件,如果是这种情况,你可以使用{{3}如果你有不同的目的,你也可以学习如何处理这种实现。而不是重新创建整个事物。

答案 3 :(得分:0)

如果事件本身已在事件中处理过,您是否传递了信息?

const handleClick = e => {
    if (!e.nativeEvent.wasProcessed) {
      // do something
      e.nativeEvent.wasProcessed = true;
    }
  };

这样,外部组件可以检查事件是否已被其中一个孩子处理过,而事件仍然会冒出来。

我提出了一个嵌套两个可点击组件的示例。 mousedown事件仅由单击的最内部Clickable组件用户处理:https://codesandbox.io/s/2wmxxzormy

免责声明:我找不到任何关于此的信息,这实际上是一种不好的做法,但通常修改一个你不拥有的对象可能导致例如在将来命名冲突。