考虑以下典型的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事件循环似乎按以下顺序运行这些操作:
-
OuterClickableArea
和InnerClickableArea
都是在propclickable
等于true
的情况下创建的(因为app state interactionBusy默认为false) -
外部区域注册鼠标事件处理程序
-
内部区域注册鼠标事件处理程序
-
用户在内部区域按下鼠标
-
内部区域的mouseDown侦听器触发器
-
内部区域的onClicking被触发
-
容器运行'setInteractionBusy'动作
-
redux app state
interactionBusy
设置为true
-
外部区域's mouseDown listener triggers (it' s
clickable
prop仍然是true
因为即使应用程序状态interactionBusy
是true
,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 回答
最简单的替代方法是在触发stopPropogation之前添加一个标志,在这种情况下标志是一个参数 .
现在甚至应用程序状态或支柱甚至可以决定触发停止传播
正如@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)分派到全局状态 . 这样您就不必担心有选择地忽略/捕获冒泡事件,并且可以为那些暗示共享状态的事件更新全局事实来源而不是处理组件中的单击事件和
clickable
状态,使用LOCK
状态到Redux状态,当用户单击时,组件只发出CLICK
操作,保持锁定并执行逻辑 . 像这样:已创建OuterClickableArea和InnerClickableArea
Redux状态,字段
CLICK_LOCK
默认为false
.外部区域注册鼠标事件处理程序
内部区域注册鼠标事件处理程序
用户单击内部区域
内部区域的
mouseDown
侦听器触发内部区域发出动作
INNER_MOUSEDOWN
外区的
mouseDown
侦听器触发外部区域发出动作
OUTER_MOUSEDOWN
INNER_MOUSEDOWN
action将CLICK_LOCK
状态设置为true
并执行逻辑(loginner area click
) .因为
CLICK_LOCK
是true
,OUTER_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 . 对未来不确定 .
在这种情况下,您可以使用react-overlays而不是重新创建整个事物,如果您有不同的目的,您还可以了解它们如何处理这种实现 .
如果事件本身已在事件中处理过,您是否传递了信息?
这样,外部组件可以检查事件是否已经被其中一个孩子处理过,而事件仍然会冒出来 .
我已经提出了一个嵌套两个Clickable组件的示例 . mousedown事件仅由单击的最内部Clickable组件用户处理:https://codesandbox.io/s/2wmxxzormy
免责声明:我找不到任何关于此的信息确实是一种不好的做法,但通常修改一个你不拥有的对象会导致例如在将来命名冲突 .