如何用 React 和鼠标事件传播实现 re-useable 组件?

How to achieve re-useable components with React and mouse 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 除了 class 名称和控制台日志语句外,代码几乎相同。

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

  1. 外部区域注册鼠标事件处理程序
  2. 内部区域注册鼠标事件处理程序
  3. 用户在内部区域按下鼠标
  4. 内部区域的mouseDown监听触发
  5. 外部区域的 mouseDown 侦听器触发
  6. 用户松开鼠标
  7. 内部区域的mouseUp监听触发
  8. 控制台日志"inner area click"
  9. 外区的mouseUp监听触发
  10. 控制台日志"outer area click"

这显示了典型事件 bubbling/propagation -- 到目前为止没有意外。

它将输出:

inner area click
outer area click

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

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

InnerClickableArea.js

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

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

inner area click

问题?

InnerClickableArea 特此隐式选择 OuterClickableArea(以及所有其他 parents)无法接收事件。尽管 InnerClickableArea 不应该知道 OuterClickableArea 的存在。就像 OuterClickableArea 不知道 InnerClickableArea (遵循关注点分离和可重用性概念)。我们在两个组件之间创建了隐式依赖关系,其中 OuterClickableArea "knows" 它不会错误地触发其侦听器,因为它 "remembers" 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>

其中 this.props.setInteractionBusy 是一个 redux 动作,会导致应用程序状态更新。此外,此容器会将应用程序状态映射到其道具(上面未显示),因此将定义 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 都是使用等于 true 的属性 clickable 创建的(因为应用程序状态 interactionBusy 默认为 false)
  2. 外部区域注册鼠标事件处理程序
  3. 内部区域注册鼠标事件处理程序
  4. 用户在内部区域按下鼠标
  5. 内部区域的mouseDown监听触发
  6. 内部区域的 onClicking 被触发
  7. 容器 运行s 'setInteractionBusy' 动作
  8. redux 应用程序状态 interactionBusy 设置为 true
  9. 外部区域的 mouseDown 侦听器触发器(它的 clickable 属性仍然是 true 因为即使应用程序状态 interactionBusytrue,反应还没有导致 re-render;渲染操作放在Javascript事件循环的末尾)
  10. 用户松开鼠标
  11. 内部区域的mouseUp监听触发
  12. 控制台日志"inner area click"
  13. 外区的mouseUp监听触发
  14. 控制台日志"outer area click"
  15. 反应 re-renders Component.jsx 并将 clickable 作为 false 传递给两个组件,但现在为时已晚

这将输出:

inner area click
outer area click

而所需的输出是:

inner area click

因此,应用程序状态无助于尝试使这些组件更加独立和可重用。原因似乎是即使更新了状态,容器在事件循环结束时仅 re-renders,鼠标事件传播触发器已经在事件循环中排队。

因此,我的问题是:除了使用应用程序状态能够以声明方式处理鼠标事件传播之外,是否有其他方法,或者是否有更好的方法implement/execute 我上面的应用程序 (redux) 状态设置?

正如@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 中,实现该目的的唯一方法是停止传播或以某种方式确定要忽略的事件。就个人而言,我会停止所有事件的传播,对于那些您有选择地希望在组件之间共享状态的单击事件,捕获事件并将操作(如果使用 redux)分派到全局状态。这样您就不必担心选择性 ignoring/capturing 冒泡事件,并且可以为那些暗示共享状态的事件更新全局真实来源

不是在组件中处理点击事件和 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. 除非点击逻辑更改某些状态,否则无需重新呈现任何内容。

注:

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

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

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

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

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

我怀疑你要创建类似覆盖模式的东西,其中有一个组件应该在单击外部组件或容器组件时关闭,如果是这种情况,你可以使用 react-overlays 如果你有不同的目的,你也可以了解他们如何处理这种实现,而不是重新创建整个东西。

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

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

这样,外部组件可以检查事件是否已被其中一个子组件处理,同时事件仍会冒泡。

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

免责声明:我找不到关于这完全是一种不良做法的任何信息,但通常修改您不拥有的对象可能会导致例如未来的命名冲突。