首页 文章

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

提问于
浏览
9

考虑以下典型的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除了类名和控制台日志语句之外几乎有相同的代码 .

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

  • 外部区域注册鼠标事件处理程序

  • 内部区域注册鼠标事件处理程序

  • 用户在内部区域按下鼠标

  • 内部区域的mouseDown侦听器触发器

  • 外部区域的mouseDown侦听器触发器

  • 用户释放鼠标

  • 内部区域的mouseUp侦听器触发

  • 控制台日志"inner area click"

  • 外部区域的mouseUp侦听器触发

  • 控制台日志"outer area click"

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

它将输出:

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

The problem?

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

我正在尝试不使用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>

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事件循环似乎按以下顺序运行这些操作:

  • OuterClickableAreaInnerClickableArea 都是在prop clickable 等于 true 的情况下创建的(因为app state interactionBusy默认为false)

  • 外部区域注册鼠标事件处理程序

  • 内部区域注册鼠标事件处理程序

  • 用户在内部区域按下鼠标

  • 内部区域的mouseDown侦听器触发器

  • 内部区域的onClicking被触发

  • 容器运行'setInteractionBusy'动作

  • redux app state interactionBusy 设置为 true

  • 外部区域's mouseDown listener triggers (it' s clickable prop仍然是 true 因为即使应用程序状态 interactionBusytrue ,react仍然没有导致重新渲染;渲染操作放在Javascript事件循环的末尾)

  • 用户释放鼠标

  • 内部区域的mouseUp侦听器触发

  • 控制台日志"inner area click"

  • 外部区域的mouseUp侦听器触发

  • 控制台日志"outer area click"

  • 反应重新渲染 Component.jsx 并将 clickable 作为 false 传递给两个组件,但到现在为时已晚

这将输出:

inner area click
outer area click

而期望的输出是:

inner area click

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

My question, therefore, is :是否有使用app状态的替代方法可以使用鼠标事件以声明方式传播,还是有更好的方法来实现/执行我的app(redux)状态的上述设置?

5 回答

  • 1

    最简单的替代方法是在触发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

    正如@Rikin所提到的,正如您在答案中提到的那样,event.stopPropagation将以当前形式解决您的问题 .

    I'm trying not to use stopPropagation to make the app more scalable, because it would be easier to add new features that rely on events without having to remember which components use stopPropagation at which time.

    就组件架构的当前范围而言,这不是一个不成熟的问题吗?如果您希望 innerComponent 嵌套在 outerClickableArea 中,实现这一目标的唯一方法是stopPropagation或以某种方式确定要忽略的事件 . 就个人而言,我会停止所有事件的传播,对于那些您有选择地想要在组件之间共享状态的点击事件,捕获事件并将操作(如果使用redux)分派到全局状态 . 这样您就不必担心有选择地忽略/捕获冒泡事件,并且可以为那些暗示共享状态的事件更新全局事实来源

  • 2

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

    • 已创建OuterClickableArea和InnerClickableArea

    • Redux状态,字段 CLICK_LOCK 默认为 false .

    • 外部区域注册鼠标事件处理程序

    • 内部区域注册鼠标事件处理程序

    • 用户单击内部区域

    • 内部区域的 mouseDown 侦听器触发

    • 内部区域发出动作 INNER_MOUSEDOWN

    • 外区的 mouseDown 侦听器触发

    • 外部区域发出动作 OUTER_MOUSEDOWN

    • INNER_MOUSEDOWN action将 CLICK_LOCK 状态设置为 true 并执行逻辑(log inner area click ) .

    • 因为 CLICK_LOCKtrueOUTER_MOUSEDOWN 什么都不做 .

    • 内部区域的 mouseUp 侦听器触发

    • 内部区域发出动作 INNER_MOUSEUP

    • 外部区域的 mouseUp 侦听器触发

    • 外部区域发出动作 OUTER_MOUSEUP

    • INNER_MOUSEUP action将 CLICK_LOCK 状态设置为 false .

    • OUTER_MOUSEUP 操作将 CLICK_LOCK 状态设置为 false .

    • 除非点击逻辑改变某些状态,否则不必重新渲染 .

    Note:

    • 这种方法通过在一个区域内锁定,一次只能有一个组件 click . 每个组件不必了解其他组件,但需要一个全局状态(Redux)来协调 .

    • 如果点击率很高,这种方法的表现可能不太好 .

    • 当前Redux操作是guaranteed to be emitted synchronously . 对未来不确定 .

  • 3

    在这种情况下,您可以使用react-overlays而不是重新创建整个事物,如果您有不同的目的,您还可以了解它们如何处理这种实现 .

  • 0

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

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

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

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

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

相关问题