Skip to main content

Chrome TouchEvent passive 参数介绍

· 15 min read

在 Chrome 浏览器中使用以下代码:

document.body.addEventListener("touchmove", (e) => {
console.log(e);
})

会在控制台看到一个警告信息:

[Violation] Added non-passive event listener to a scroll-blocking 'touchmove' event. Consider marking event handler as 'passive' to make the page more responsive. See https://www.chromestatus.com/feature/5745543795965952

解决办法就是传递第三个参数, 显式设置 passive 的值. 大多数情况下, 是直接在 addEventListener 函数的第三个参数传递 { passive: true }.

原因

产生这个警告的原因是, 默认情况下在 touchstart 和 第一个touchmove 事件的回调函数中可以通过调用 event.preventDefault() 来阻止页面触摸滚动(Chrome 56 之前版本), 为了支持这种事件行为, 默认情况下, 页面的触摸滚动就要等待第一个 touchmove 事件结束后才可以滚动, 这会导致严重的滚动性能问题.

note

注意在 Chrome 56 版本之前, passive 默认为 false. 为了用户体验, 在之后版本中默认 true.

为了解决该问题, addEventListenner 第三个参数 引入了 passive 选项:

  • 如果设置了 passive: true, 则页面触摸滚动就不需要等待第一个touchmove 事件结束, 也意味着 event.preventDefault() 方法不会起作用.
  • 如果设置了 passive: false, 不管是否需要调用 e.preventDefault() 来阻止页面滚动,都需要等到第一个 touchmove 函数执行完毕,页面才会做出反应, 也就可以在 touchmove事件回调函数内调用event.preventDefault()来阻止页面滚动.
danger

注意官方不建议使用 { passtive: false } + event.preventDefault() 的方式阻止滚动, 如果实际需求中需要阻止触摸滚动, 使用CSS样式 touch-action: none 或者 touch-action: pan-y pinch-zoom(水平滚动) 来阻止触摸滚动:

/* 禁止滚动和缩放 */
element {
touch-action: none;
}

/* 只允许垂直滚动 */
element {
touch-action: pan-y;
}

/* 只允许水平滚动 */
element {
touch-action: pan-x;
}

/* 启用垂直滚动和缩放 */
element {
touch-action: pan-y pinch-zoom;
}

参考资料

Making touch scrolling fast by default

We know that scrolling responsiveness is critical to the user's engagement with a website on mobile, yet touch event listeners often cause serious scrolling performance problems. Chrome has been addressing this by allowing touch event listeners to be passive (passing the {passive: true} option to addEventListener()) and shipping the pointer events API. These are great features to drive new content into models that don't block scrolling, but developers sometimes find them hard to understand and adopt. 我们知道,在移动设备上,滚动响应对用户与网站的互动至关重要,然而触摸事件监听器常常导致严重的滚动性能问题。Chrome 通过允许触摸事件监听器变为被动(将 {passive: true} 选项传递给 addEventListener())并实现指针事件 API 来解决这个问题。这些功能对于引导新内容到不阻塞滚动的模型来说非常有用,但开发者有时会觉得它们难以理解和采纳。

We believe the web should be fast by default without developers needing to understand arcane details of browser behavior. In Chrome 56 we are defaulting touch listeners to passive by default in cases where that most often matches the developer's intention. We believe that by doing this we can greatly improve the user's experience whilst having minimal negative impact on sites. 我们相信,网络应该在默认情况下就很快,而无需开发者了解浏览器行为的奥秘。在 Chrome 56 中,我们将触摸监听器默认为被动的,在这种情况下,它通常与开发者的意图相匹配。我们相信,通过这样做,我们可以在对网站的负面影响最小的情况下,极大地提高用户的体验。

In rare cases this change can result in unintended scrolling. This is usually easily addressed by applying a touch-action: none style to the element where scrolling shouldn't occur. Read on for details, how to know if you are impacted, and what you can do about it. 在极少数情况下,这种变化可能导致意外的滚动。通常通过将 touch-action: none 样式应用于不应发生滚动的元素,可以很容易地解决这个问题。请继续阅读了解详细信息,了解您是否受到影响,以及您可以采取哪些措施。

Background: Cancelable Events slow your page down

背景: 可取消事件会让你的页面变慢

note

这里的 Cancelable Events 是指使用 event.preventDefault() 方法阻止 touch 事件阻止页面滚动.

If you call preventDefault() in the touchstart or first touchmove events then you will prevent scrolling. The problem is that most often listeners will not call preventDefault(), but the browser needs to wait for the event to finish to be sure of that. Developer-defined "passive event listeners" solve this. When you add a touch event with a {passive: true} object as the third parameter in your event handler then you are telling the browser that the touchstart listener will not call preventDefault() and the browser can safely perform the scroll without blocking on the listener. For example: 如果您在 touchstart 或第一个 touchmove 事件中调用 preventDefault(),那么您将阻止滚动。问题在于,大多数情况下,监听器(事件回调函数)不会调用 preventDefault(),但浏览器需要等待事件结束才能确保这一点。开发者定义的 passive event listeners 解决了这个问题。当您在事件处理程序中将 {passive: true} 对象作为第三个参数添加触摸事件时,您就告诉浏览器 touchstart 监听器不会调用 preventDefault(),浏览器可以在不阻塞监听器的情况下安全地执行滚动。例如:

window.addEventListener("touchstart", func, { passive: true });

The Intervention

Our main motivation is to reduce the time it takes to update the display after the user touches the screen. To understand the usage of touchstart and touchmove we added metrics to determine how frequently scroll blocking behavior occurred. 我们的主要动机是减少用户触摸屏幕后更新显示所需的时间。为了理解 touchstarttouchmove 的使用情况,我们添加了指标来确定滚动阻塞行为发生的频率。

We looked at the percentage of cancelable touch events that were sent to a root target (window, document, or body) and determined that about 80% of these listeners are conceptually passive but were not registered as such. Given the scale of this problem we noticed a great opportunity to improve scrolling without any developer action by making these events automatically "passive". 我们查看了发送到根节点(windowdocumentbody)的可取消触摸事件的百分比,并确定了大约 80% 的这些监听器在概念上是被动的,但并未作为此类进行注册。鉴于这个问题的规模,我们发现了一个很好的机会来改进滚动,而无需任何开发者操作,那就是通过自动将这些事件变为“被动”。

This drove us to define our intervention as: if the target of a touchstart or touchmove listener is the window, document or body we default passive to true. This means that code like: 这使我们将干预定义为:如果 touchstarttouchmove 监听器的目标是windowdocumentbody,我们将passive属性默认设置为 true。这意味着像这样的代码:

window.addEventListener("touchstart", func);

becomes equivalent to: 等价于:

window.addEventListener("touchstart", func, {passive: true} );

Now calls to preventDefault() inside the listener will be ignored. 现在在事件监听器中调用 preventDefault() 将会被忽略.

Breakage and Guidance

In the vast majority of cases, no breakage will be observed. But when breakage does occur, the most common symptom is that scrolling happens when you don't want it. In rare cases, developers may also notice unexpected click events (when preventDefault() was missing from a touchend listener). 在绝大多数情况下,该默认行为的变化(指Chrome 56 从 {passive: false} 变成了 {passive: true} 导致 touchmove 事件监听器中的 event.preventDefault() 不再起作用)不会出现问题。但当问题确实发生时,最常见的症状是在您不希望滚动时发生滚动。在极少数情况下,开发者可能还会注意到意外的点击事件(当 touchend 监听器中缺少 preventDefault()) 时)。

In Chrome 56 and later, DevTools will log a warning when you call preventDefault() in an event where the intervention is active. 在 Chrome 56 及更高版本中,当您在启用干预的事件中调用 preventDefault() 时,DevTools 将打印一个警告。

touch-passive.html:19 Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/features/5093566007214080

Your application can determine whether it may be hitting this in the wild by checking if calling preventDefault had any effect via the defaultPrevented property. 您的应用程序可以通过检查调用 preventDefault 是否对 defaultPrevented 属性产生了任何影响来判断它是否在实际场景中遇到了这个问题。

We've found that a large majority of impacted pages are fixed relatively easily by applying the touch-action CSS property whenever possible. If you wish to prevent all browser scrolling and zooming within an element apply touch-action: none to it. If you have a horizontal carousel consider applying touch-action: pan-y pinch-zoom to it so that the user can still scroll vertically and zoom as normal. Applying touch-action correctly is already necessary on browsers such as desktop Edge that support Pointer Events and not Touch Events. For mobile Safari and older mobile browsers that don't support touch-action your touch listeners must continue calling preventDefault even when it will be ignored by Chrome. 我们发现,绝大多数受影响的页面通过尽可能应用 touch-action CSS 属性可以相对容易地修复。如果您希望在元素内阻止所有浏览器滚动和缩放,请将 touch-action: none 应用于它。如果您有一个水平轮播,请考虑将 touch-action: pan-y pinch-zoom 应用于它,以便用户仍然可以正常地垂直滚动和缩放。在支持指针事件而不是触摸事件的浏览器(如桌面 Edge)上,正确应用 touch-action 已经是必要的。对于不支持 touch-action 的移动 Safari 和较旧的移动浏览器,即使 Chrome 会忽略它,您的触摸监听器仍然必须在调用 preventDefault 时继续调用。

In more complex cases it may be necessary to also rely on one of the following: 在更复杂的情况下,可能还需要依赖以下方法之一:

If your touchstart listener calls preventDefault(), ensure preventDefault() is also called from associated touchend listeners to continue suppressing the generation of click events and other default tap behavior. 如果您的 touchstart 监听器调用了 preventDefault(),请确保 preventDefault() 也从关联的 touchend 监听器中调用,以继续阻止 click 事件和其他默认点击行为的产生。

Last (and discouraged) pass {passive: false} to addEventListener() to override the default behavior. Be aware you will have to feature detect if the User Agent supports EventListenerOptions. 最后(并不鼓励),将 {passive: false} 传递给 addEventListener() 以覆盖默认行为。请注意,您需要检测用户代理是否支持 EventListenerOptions 功能。

检测浏览器是否支持 passtive 属性
let passiveSupported = false;

try {
const options = Object.defineProperty({}, "passive", {
get: function() {
passiveSupported = true;
}
});

window.addEventListener("test", options, options);
window.removeEventListener("test", options, options);
} catch (err) {
passiveSupported = false;
}

const eventOptions = passiveSupported ? { passive: false } : false;
window.addEventListener("touchstart", yourFunction, eventOptions);

上述代码首先通过尝试设置并检测 passive 属性来检测浏览器是否支持 EventListenerOptions。如果支持,passiveSupported 变量将设置为 true。然后,根据 passiveSupported 变量的值,为 addEventListener() 创建适当的事件选项对象,其中将 passive 设置为 false。这样可以覆盖默认行为,但请注意,这种方法并不鼓励使用,因为它可能会影响页面滚动性能。

Conclusion

In Chrome 56 scrolling starts substantially faster on many websites. This is the only impact that most developers will notice as a result of this change. In some cases developers may notice unintended scrolling. 在 Chrome 56 中,许多网站的滚动速度明显加快。这是大多数开发者会注意到的此次更改带来的唯一影响。在某些情况下,开发者可能会注意到意外的滚动。

Although it's still necessary to do so for mobile Safari, websites should not rely on calling preventDefault() inside of touchstart and touchmove listeners as this is no longer guaranteed to be honored in Chrome. Developers should apply the touch-action CSS property on elements where scrolling and zooming should be disabled to notify the browser before any touch events occur. To suppress the default behavior of a tap (such as the generation of a click event), call preventDefault() inside of a touchend listener. 尽管仍然需要为移动 Safari 这样做,但网站不应依赖在 touchstarttouchmove 监听器中调用 preventDefault(),因为在 Chrome 中不再保证会遵守该操作。开发者应在需要禁用滚动和缩放的元素上应用 touch-action CSS 属性,以在任何触摸事件发生之前通知浏览器。要抑制点击行为的默认行为(例如 click 事件的生成),请在 touchend 监听器中调用 preventDefault()

Updated on Tuesday, July 31, 2018