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

Tom*_*Tom 9 javascript race-condition event-propagation reactjs redux

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

Component.jsx

<OuterClickableArea>
    <InnerClickableArea>
        content
    </InnerClickableArea>
</OuterClickableArea>
Run Code Online (Sandbox Code Playgroud)

这些组件的组成如下:

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>
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

  1. 外部区域注册鼠标事件处理程序
  2. 内部区域注册鼠标事件处理程序
  3. 用户在内部区域按下鼠标
  4. 内部区域的mouseDown侦听器触发器
  5. 外部区域的mouseDown侦听器触发器
  6. 用户释放鼠标
  7. 内部区域的mouseUp侦听器触发器
  8. 控制台记录"内部区域点击"
  9. 外部区域的mouseUp侦听器触发器
  10. 控制台日志"外部区域点击"

这显示了典型的事件冒泡/传播 - 到目前为止没有惊喜.

它将输出:

inner area click
outer area click
Run Code Online (Sandbox Code Playgroud)

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

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

InnerClickableArea.js

    onMouseDown(e) {
        if (!this.state.clicking) {
            e.stopPropagation()
            this.setState({ clicking: true })
        }
    }
Run Code Online (Sandbox Code Playgroud)

这按预期工作.它将输出:

inner area click
Run Code Online (Sandbox Code Playgroud)

问题?

InnerClickableArea特此暗中选择OuterClickableArea(和所有其他父母)不能接收活动.即使InnerClickableArea不应该知道存在OuterClickableArea.就像OuterClickableArea不知道InnerClickableArea(关注分离和可重用性概念).我们在两个组件之间创建了一个隐式依赖关系,其中OuterClickableArea"知道"它不会错误地触发其侦听器,因为它"记住"InnerClickableArea将停止任何事件传播.这似乎是错的.

我正在尝试不使用stopPropagation来使应用程序更具可伸缩性,因为添加依赖于事件的新功能会更容易,而不必记住哪些组件stopPropagation在哪个时间使用.

相反,我想使上述逻辑更具说明性,例如通过在内部声明地定义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>
Run Code Online (Sandbox Code Playgroud)

哪个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>
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  1. 二者OuterClickableAreaInnerClickableArea用丙创建clickable等于true(因为应用程序状态interactionBusy默认为false)
  2. 外部区域注册鼠标事件处理程序
  3. 内部区域注册鼠标事件处理程序
  4. 用户在内部区域按下鼠标
  5. 内部区域的mouseDown侦听器触发器
  6. 内部区域的onClicking被触发
  7. 容器运行'setInteractionBusy'动作
  8. redux app state interactionBusy设置为true
  9. 外部区域的mouseDown监听器触发器(它的clickable支持仍然true是因为即使应用程序状态interactionBusytrue,react还没有导致重新渲染;渲染操作放在Javascript事件循环的末尾)
  10. 用户释放鼠标
  11. 内部区域的mouseUp侦听器触发器
  12. 控制台记录"内部区域点击"
  13. 外部区域的mouseUp侦听器触发器
  14. 控制台日志"外部区域点击"
  15. 反应重新呈现Component.jsx并将在clickablefalse这两个组件,但现在这是为时已晚

这将输出:

inner area click
outer area click
Run Code Online (Sandbox Code Playgroud)

而期望的输出是:

inner area click
Run Code Online (Sandbox Code Playgroud)

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

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

kar*_*hik 2

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

const onMouseDown = (stopPropagation) => {
stopPropagation && event.stopPropagation();
}
Run Code Online (Sandbox Code Playgroud)

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

<div onMouseDown={this.onMouseDown(this.state.stopPropagation)} onMouseUp={this.onMouseUp(this.props.stopPropagation)} >
    <InnerComponent stopPropagation = {true}>
</div>
Run Code Online (Sandbox Code Playgroud)