React: 支持被动事件监听器

创建于 2016-04-07  ·  62评论  ·  资料来源: facebook/react

https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md

默认情况下让所有内容都处于被动状态并且仅在需要时才选择加入活动状态会很好。 例如,您可以侦听文本输入事件,但只有在有活动侦听器时才阻止默认或使用受控行为。

同样,我们可以将其与 React Native 的线程模型统一起来。 例如,当有活动侦听器(例如处理击键)时,我们可以做的一件事是同步阻塞 UI 线程。

抄送@vjeux @ide

DOM React Core Team Big Picture Feature Request

最有用的评论

我刚刚在 chrome 中遇到了关于处理滚轮事件的警告,如果将其注册为被动事件处理程序,则可以对其进行优化。 所以在 React 中使用它会很整洁!

所有62条评论

这已登陆 Chrome 51。是否有任何更新计划在 React 中支持此功能? :O

如果 React 在文档上只有一个事件侦听器,然后委托给其他人,这怎么可能呢?
@sebmarkbage

被动事件问题的当前状态是什么?

我刚刚在 chrome 中遇到了关于处理滚轮事件的警告,如果将其注册为被动事件处理程序,则可以对其进行优化。 所以在 React 中使用它会很整洁!

您还需要处理任意选项,例如每晚已经登陆 Firefox 的oncehttps : https :

FWIW,当侧边栏或聊天窗口滚动时,Facebook 会侦听活动的轮子事件以阻止外部滚动。 没有它我们就无法实现 UI。 我们仍然希望支持此选项,但问题空间仍然不完整,因此可能会针对此问题提出不涉及被动事件侦听器的替代解决方案。 所以它仍然是一个活跃的设计空间。

保持主动侦听器并添加支持被动侦听器很重要。
在桌面应用程序上,您看不到任何区别,但在移动应用程序上,被动滚动侦听器可大大提高速度。

小建议:

<SomeElement
  onScroll={this.onScrollThatCallsPreventDefault}
  onScrollPassive={this.onScrollThatJustListens}
  ...this.props
/>

@romulof是的,这也是您在捕获阶段注册事件的方式

<SomeElement
  onClick={this.onClick}
  onClickCapture={this.onClickCapture}
  onScrollPassive={this.onScrollPassive}
/>

所以我想这也是支持被动事件的合适 API。

旁注:一个棘手的问题是 - 您将如何为捕获阶段注册被动事件? 我想这是不可能的,因为被动事件的性质。 由于他们甚至不允许调用event.preventDefault() ,所以这可能不是问题。

@radubreharonScrollCapturePassive看起来像骆驼式的整本圣经。

:) 事实并非如此,因为在捕获阶段没有被动事件。

当然这没有意义,但我不会指望它。 还有其他类型的事件绑定,例如once

另一个建议:

<SomeElement
  onScroll={this.onScrollThatCallsPreventDefault}
/>
<SomePassiveElement
  onScroll={{
    passive: true,
    capture: true,
    handler: this.onScrollThatJustListens,
  }}
/>

这样 React 就必须检测事件处理程序是一个函数(普通绑定),还是包含绑定选项和处理程序函数的对象。

我认为带有选项的对象方法比onFooPassive更有意义,因为可能还需要其他选项。 如果结合@sebmarkbage的建议,默认情况下事件应该是被动的,这可能不会太麻烦。

想到的另一种方法是将属性附加到事件处理程序,以允许它们选择退出被动模式(或切换其他选项)。 像这样的东西:

class Foo extends React.Component {
  constructor() {
    this.handleScroll = this.handleScroll.bind(this);
    this.handleScroll.passive = false;
  }

  handleScroll() {
    ...
  }

  render() {
    return <div onScroll={this.handleScroll} />;
  }
}

从理论上讲,一旦他们着陆,这对装饰者来说会很好地工作。

再考虑一下,我认为最好为函数添加一个事件选项属性,而不是单个选项。 这将允许 React 只需要担心一个属性而不是潜在的多个属性。 所以,要调整我上面的例子:

class Foo extends React.Component {
  constructor() {
    this.handleScroll = this.handleScroll.bind(this);
    this.handleScroll.options = { passive: false };
  }

  handleScroll() {
    ...
  }

  render() {
    return <div onScroll={this.handleScroll} />;
  }
}

我想到的另一个想法是,如果我们以允许通过 JSX 传递这些选项的方式修改 JSX 语法,这会是什么样子。 这是一个我没有考虑太多的随机示例:

return <div onScroll={this.handleScroll, { passive: false }} />;

我也一直在考虑事件是否应该默认是被动的,我有点犹豫不决。 一方面,这对于像滚动处理程序这样的事件当然是很好的,但我担心它会导致许多点击处理程序的过多动荡和意外行为。 我们可以让一些事件在默认情况下是被动的,而另一些则不是,但这可能最终会让人们感到困惑,所以可能不是一个好主意。

这种方式与我之前提出的非常相似,没有修改 JSX 语法。

return <div onScroll={{ handler: this.handleScroll, passive: true }} />;

文档很简单:

div.propTypes = {
  ...
  onScroll: React.PropTypes.oneOf([
    React.PropTypes.func,
    React.PropTypes.shape({
      handler: React.PropTypes.func.isRequired,
      capture: React.PropTypes.bool,
      passive: React.PropTypes.bool,
      once: React.PropTypes.bool,
    }),
};

默认情况下反应事件是被动的吗? 至少对于触摸事件来说似乎是这样。 我无法preventDefault除非我退回到普通文档级事件侦听器。

@joshjg React 处理程序被传递“合成事件”,这有点像原生事件,但不同。 顺便说一句,有更多知识的人应该更正我要说的内容,因为我实际上还没有阅读执行此操作的代码。

我不太熟悉实现细节,但我知道preventDefault至少可以工作_只要您阻止的处理程序也是 React 事件处理程序_。 反正这是我的经验。

使用stopPropagation您更有可能不走运(例如,您有一个document点击侦听器,它无法与 React 绑定,并且您想避免在点击内部时冒泡某个元素)。 在这种情况下,您可以使用:

function stopPropagation (e) {
  e.stopPropagation();
  e.nativeEvent.stopImmediatePropagation();
}

[[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopImmediatePropagation)]

这稍微偏离了主要话题,但简短的回答是 React 不使用被动事件,只是有时以奇怪的顺序处理它们。

@joshjg @benwiley4000 @gaearon最近 chrome 团队改变了他们处理文档级触摸事件的方法,默认情况下使它们处于被动状态。 由于 React 在文档级别附加事件,因此您会获得这种新行为。

https://www.chromestatus.com/features/5093566007214080

这间接地改变了 React 的行为方式——我想 React 在附加事件时没有明确提及passive: false ——因此行为发生了变化。

我也只是点击了这个 - 所以你需要手动注册触摸事件,使用 addEventListener

请注意,Chrome 被动默认干预仅适用于touchstarttouchmove ,而不适用于wheel 。 因此,没有显式{passive: true}wheel事件仍将强制同步滚动,用于鼠标滚轮和两指触控板滚动。 (我写了一篇关于这里的一些微妙之处的

此外,我们(Edge 团队)无意实施相同的干预,因此当我们发布被动事件侦听器时,您仍然需要明确指定{passive: true}

仅供参考,我开始沿着passive: false 路径来防止身体在有滚动div 时在移动设备上滚动,但是使用preventDefault() 来阻止滚动有点繁重。 我可以根据 div 是否存在添加和删除处理程序,或者退回到 body.height = 100% 方法。 body.height 修复感觉有点hacky,但是我根本不需要passive: false。

我的用例是,当用户在其中拖动元素时,我想使用event.preventDefault()方法来防止容器滚动。

为此,我需要将事件侦听器注册为非被动(被动:false)。
由于浏览器正在切换到被动:默认情况下为 true,我希望能够做相反的事情

不幸的是,我无法使用touch-action: none;样式,因为它是在触摸开始后应用的,这可能就是它没有任何效果的原因。

这确实很快就会成为一个问题,我很惊讶在两年内没有找到解决方案。 手动创建事件侦听器是 React 中的一种反模式。

如果它带来了突破性的变化,那就这样吧。 不过我可能漏掉了故事的一部分。

我喜欢@romulofhttps://github.com/facebook/react/issues/6436#issuecomment -254331351 中提出的新事件监听器签名。
除了修复这里描述的问题之外,还可以指定其他 EventListenerOptions,例如once

我刚刚遇到了这个问题。 我有一个用户可以画画的画布。 在 Android 上绘图时,它有时会“拉动刷新”而不是进行绘画描边。 这表明这是一个现实世界的问题。 我现在将避免使用onTouch{Start,Move,End}并手动使用addEventListener作为解决方法。

我非常喜欢@romulof建议的方法。 似乎解决方案也不需要重大更改。

@bobvanderlindentouch-action: none;到您的画布样式应该对您有用,它真的很适合该用例。 其他可能的值也非常方便。

然而,正如@piotr-cz 指出的那样, touch-action并不能从整体上普遍解决这个被动事件问题。 我也遇到了同样的问题,即在拖动子元素时阻止容器滚动。 所有的变通方法都非常棘手,并增加了技术债务。

不幸的是,任何非被动侦听器的存在都可能导致严重的卡顿,即使没有实际连接到它的用户空间处理程序。 Chrome 会在verbose日志级别告诉您: [Violation] Handling of 'wheel' input event was delayed for 194 ms due to main thread being busy. Consider marking event handler as 'passive' to make the page more responsive. (这是由 https://github.com/facebook/react/blob/92b7b172cce9958b846844f0b46fd7bbd8c5140d/packages/ 添加的顶级处理程序react-dom/src/events/ReactDOMEventListener.js#L155)

@romulof @lencioni @radubrehar您是否知道被动标志不适用于滚动事件侦听器? 它应该用于touchmove等事件,以免干扰浏览器的滚动性能。 你的例子让我非常困惑。

设置被动对于基本滚动事件并不重要,因为它无法取消,因此它的侦听器无论如何都无法阻止页面呈现。

来源: https :

附加信息: https :

是的,我现在知道这一点。 我之前写的时候被误导了
注释。 谢谢澄清!

2017 年 12 月 6 日,星期三,上​​午 8:25 Martin Hofmann通知@ github.com
写道:

@romulof https://github.com/romulof @lencioni
https://github.com/lencioni @radubrehar https://github.com/radubrehar
您是否知道被动标志不适用于
滚动事件监听器? 它应该用于 touchmove 等事件。
不干扰浏览器的滚动性能。 你的例子
对我来说非常困惑。

设置被动对于基本滚动事件并不重要,因为它不能
被取消,因此它的侦听器无论如何都无法阻止页面呈现。

来源:
https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Improving_scrolling_performance_with_passive_listeners

附加信息:
https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md


你收到这个是因为你被提到了。

直接回复本邮件,在GitHub上查看
https://github.com/facebook/react/issues/6436#issuecomment-349691618
或静音线程
https://github.com/notifications/unsubscribe-auth/AAL7zgbNCpHNui-TX7r2FYdhVxZfdBX8ks5s9r_4gaJpZM4ICWsW
.

@el-moalo-loco,我很确定我在 Google Developers 网站上阅读了一些关于使用被动侦听器进行滚动事件来提高性能的文档。 我一定是误读了,或者一路上发生了一些变化。 无论如何,非常感谢您的澄清!

@romulof @lencioni @el-moalo-loco https://developers.google.com/web/tools/lighthouse/audits/passive-event-listeners
即使滚动侦听器不必如此,Wheel 侦听器仍然应该是被动的。 我认为这可能是混淆的根源。

@sebmarkbage你怎么看? 这种被动事件侦听器支持会在某个时候进入 React 吗?

你好,

我只需要在componentDidMount添加一个活动事件侦听器,如下所示:

global.addEventListener("touchstart", this.touchStart(), { passive: false })

这样我就可以调用e.preventDefault()来停止 Chrome 的默认滚动并移动touchStart()的元素。

为了知道我必须移动哪个元素,我必须像这样将onTouchStart到 JSX 元素中:

onTouchStart={this.touchStartSetElement(element)}

touchStartSetElement()我设置了一个状态属性element ,我可以在touchStart()读取它

如果 React 支持活动事件侦听器,这将归结为一行。

谢谢,

菲利普

PS:如果您尝试在被动事件侦听器中调用e.preventDefault() ,则会在 Chrome 56 中收到此错误:

[干预] 由于目标被视为被动,因此无法防止被动事件侦听器中的默认值。 见https://www.chromestatus.com/features/5093566007214080

由于谷歌的“干预”并以某种方式破坏网络,被动事件已成为 Chrome 56 的默认设置 - 但也使滚动速度更快。

这越来越成为一个问题,因为从 iOS 11.3 开始,Safari 也默认为被动,并且不支持touch-action:none的经典解决方法。

我提出了一个 RFC https://github.com/reactjs/rfcs/pull/28 ,它允许创建像 props 一样工作的自定义 ref 处理程序(即你像onClick一样使用属性,而是使用计算属性语法和处理程序获取 ref 和 prop 值信息和更新)。 这些可用于为您拥有的任何高级用例创建库。

  • 被动、明确非被动、一次性和捕获事件都可以轻松使用它。
  • 可以使用它们完成比事件处理程序注册更高级的事情。
  • 从用户的角度来看,它们易于使用,只需将库提供的内容作为计算属性传递给任何元素。 例如import {onScroll} from 'react-passive-events'; <div [onScroll]={scrollHandler} />

我不认为这应该是所有注册事件方式。

然而,与其想出复杂的方法来处理注册所有可能类型的事件(捕获、被动等...),我建议决定大多数事件(被动或非被动)的默认行为应该是什么,并使用这些注册的props 来处理更高级的用例。

在 iOS 11.3 中,所有触摸事件现在默认都是被动的。 因此在任何触摸事件处理程序中调用 event.preventDefault() 现在是无效的 😢

https://codesandbox.io/s/l4kpy569ol

由于无法强制非被动事件处理程序,我们很难解决 iOS 11.3 更改https://github.com/atlassian/react-beautiful-dnd/issues/413

我来看看 Vue.js 如何处理这个问题,我非常喜欢他们的方法

<!-- the click event's propagation will be stopped -->
<a v-on:click.stop="doThis"></a>

<!-- the submit event will no longer reload the page -->
<form v-on:submit.prevent="onSubmit"></form>

<!-- modifiers can be chained -->
<a v-on:click.stop.prevent="doThat"></a>

<!-- just the modifier -->
<form v-on:submit.prevent></form>

有这个修饰符列表:

  • 。停止
  • 。防止
  • 。捕获
  • 。自己
  • 。一次
  • 。被动的

您可以按照自己喜欢的方式编写事件。 也许这可以帮助作为这个问题的灵感。

@KeitIG我正在开发一个专为 React 设计的 Vue-loader

https://github.com/stalniy/react-webpack-loader

这让我很难过

selection_028

不确定这是否已经被建议 - 或者它对除我以外的任何人是否有意义,但是:

onTouchStart={listener} 

onTouchStart={listener, options}

既可以使传递选项(如{ passive, true, once: true }成为一种自然的方式来做到这一点,它也将匹配addEventListener模式。

转向@phaistonian的建议将消除对当今存在的任何onEventNameCapture处理程序的需求

@alexreardon我真的认为这不是一个选项,因为在普通的 js 中, listener, options是一个表达式,计算结果为options ,所以上面的构造不是你的意思。 这将需要改变 jsx 编译为 js 的方式,这将是一个突破性的变化。 我怀疑 React 团队会走这条路。

意见?

考虑到它是一个表达式,它可以以相同的意图以不同的方式完成。

可能有很多选择。 选项包括:

import {handler} from 'React';

onTouchStart={handler(listener, options)}
onTouchStart={{listener, options}}

或者

onTouchStart={[listener, options]}

或者

onTouchStart={listener} onTouchStartOptions={options}

我最喜欢传递一个对象的想法。 无论哪种方式,这都需要一个解决方案。

现在有解决方案吗?

现在有解决方案吗?

您的经典addEventListenerremoveEventListener位于componentDidMountcomponentWillUnmount

是的,这很糟糕。

你认为如何使用钩子解决它?

...
const onClickPassive = useEventListener((e) => {
 console.log('passive event')
}, { passive: true })

return (
  <button onClick={onClickPassive}>Click me</button>
)

@ara4n使用钩子是好的,但仍然需要一个经典的非钩子反应解决方案

@sebmarkbage 有任何更新吗? Chrome 刚刚发布了这个并破坏了我们的应用程序。

https://www.chromestatus.com/features/6662647093133312

[Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/features/6662647093133312
当我试图阻止onWheel事件侦听器中的默认滚动行为时,它再次发生

@kychanbi和我一样,但我只在 Windows chrome 上遇到这个错误。

@kychanbi哦,这是 Chrome 73 的一项功能,它将文档级别的 Wheel/Mousewheel 事件侦听器视为被动

您可以在组件容器 div touch-action: none 上使用 css 属性

。容器 {
触摸动作:无;
}

@madcher似乎不适用于鼠标事件。
我终于通过使用原生 javascript 解决了它
element.addEventListener("wheel", eventHandler);

一个小片段来帮助那些面临这个问题的人:

import React, { useRef, useEffect } from 'react'

const BlockPageScroll = ({ children }) => {
  const scrollRef = useRef(null)
  useEffect(() => {
    const scrollEl = scrollRef.current
    scrollEl.addEventListener('wheel', stopScroll)
    return () => scrollEl.removeEventListener('wheel', stopScroll)
  }, [])
  const stopScroll = e => e.preventDefault()
  return (
    <div ref={scrollRef}>
      {children}
    </div>
  )
}

const Main = () => (
  <BlockPageScroll>
    <div>Scrolling here will only be targeted to inner elements</div>
  </BlockPageScroll>
)

@madcher

不知何故onWheel props 不适用于 css touch-action: none;

    componentRef = React.createRef(null);
    handleWheel = (e) => {
      e.preventDefault();
    }
    render() {
      <Container style={{ touchAction: 'none' }} onWheel={this.handleWheel}>
        ...
      </Container>
    }

仍然收到此错误:
[Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive. See <URL>

工作版本

@markpradhan的 hooks 解决方案的替代方案,如果您仍在使用旧式组件,则可以执行以下操作:

    componentRef = React.createRef();
    handleWheel = (e) => {
      e.preventDefault();
    }
    componentDidMount() {
      if (this.componentRef.current) {
        this.componentRef.current.addEventListener('wheel', this.handleWheel);
      }
    }
    componentWillUnmount() {
      if (this.componentRef.current) {
        this.componentRef.current.removeEventListener('wheel', this.handleWheel);
      }
    }
    render() {
      <Container ref={this.componentRef}>...</Container>
    }

@Fonger可能是由于 # 14856

已经提出了类似的建议,但这里还有一些想法。

function MyComponent() {
  function onScroll(event) { /* ... */ }
  onScroll.options = {capture, passive, ...};
  return <div onScroll={onScroll} />;
}

这将使您轻松选择被动事件或捕获事件,而无需进行重大更改。 但是,我对默认情况下被动事件侦听器的想法很感兴趣。 我记得 preventDefault 是阻止 React 在 worker 中运行的主要障碍(除其他外)。

js function MyComponent() { function onScroll(event) { /* ... */ } onScroll.shouldPreventDefault = (event): boolean => { // some logic to decide if preventDefault() should be called. } onScroll.shouldStopPropagation = (event): boolean => { // some logic to decide if stopPropagation() should be called. } return <div onScroll={onScroll} />; }
很难确保这不会成为破坏性更改,但如果强制执行,所有决定事件是否需要preventDefault ed 的代码都将在代码中隔离,React 将能够在主线程上仅运行该部分,并在单独的工作线程中或异步运行其他所有内容。

在解决此问题之前,我认为最好从文档中删除对event.preventDefault()引用,或者至少标有关于 Chrome 无法在被动事件上preventDefault的警告。

我想知道从 React v17 更改事件委托的含义。 Lighthouse 有一个规则https://web.dev/uses-passive-event-listeners/测试非被动事件。

以前, <div onTouchStart />会在文档上注册,默认情况下是被动的。 然而,在 React v17 中,事件注册在 React 树的根上,如果没有特别要求,它不再是被动的。

复制: https : //codesandbox.io/s/material-demo-forked-e2u72?file= / demo.js ,直播: https :

Capture d’écran 2020-08-19 à 16 11 31

是的。 似乎有关。 我将提交一个新问题。

提交https://github.com/facebook/react/issues/19651进行 React 17 讨论。

此页面是否有帮助?
0 / 5 - 0 等级