你真的会用 addEventListener 吗?
没错, 的确很标题党, 但是我最近发现我的一个朋友 (谁说是我的!) 真的不太会用 addEventListener.
起因
我最近参与了一个开源项目, https://github.com/SSENSE/vue-carousel ,Vue 生态圈里的一个轮播组件, 由加拿大的一个电商公司 SSENSE 开源的. 然后刚好有人提了一个 https://github.com/SSENSE/vue-carousel/issues/174
Unable to preventDefault inside passive event listener due to target being treated as passive
也许有人在开发移动端应用的时候遇到过这个问题, 我来为大家复现一下这个场景:
当我们给 document 添加了 touch 事件的监听器的时候, 如果同时在 handler 内部调用了
event.preventDefault()
, 这时候浏览器 (Chrome 56+) 就会报一个 warning:Unable to preventDefault inside passive event listener due to target being treated as passive
这句警告翻译过来就是: 不能给 passive(被动的)事件监听器 preventDefault, 因为它被认为是 passive.
什么意思呢? 不着急, 首先, 我们来看一下什么叫
passive event listener
. 但是在这之前, 我们还是得复习一下 addEventListener 的第三个参数.
不为所知的第三个参数
当我们使用 addEventListener 的时候, 我们一般的写法是以下:
target.addEventListener(event, handler)
相信还有人使用更完整的写法:
target.addEventListener(event, handler, false)
是的, 相信很多人也已经知道了, addEventListener 方法是有第三个参数的, 我们时常传入一个 false 来作为这第三个参数. 但是我们不传其实也一样, 因为这第三个参数默认就是 false, 是不是觉得自己不明所以地写了很多冤枉代码还自以为很严谨?
哈哈, 所以我们得来看看, 这第三个参数到底是做什么的.
常规操作下, 这第三个参数是一个布尔值, 叫 useCapture, 也就是指在 DOM 树中, 注册了该 listener 的元素, 是否会先于它下方的任何事件目标, 接收到该事件.
我们知道, DOM 事件流 (event flow) 存在三个阶段: 事件捕获阶段, 处于目标阶段, 事件冒泡阶段. 如果 useCapture 设置为 false, 当前 eventTarget 就不会在捕获阶段接收该事件. 浏览器默认我们不会在捕获阶段触发绑定事件的 handler.
但是我相信还是有很多人没有认真看过 addEventListener 的文档, 第三个参数其实并不一定是一个布尔值. 他也可以是一个对象, 一组配置.
- {
- capture: Boolean, // 表示 `listener` 会在该类型的事件捕获阶段传播到该 `EventTarget` 时触发
- once: Boolean, // 表示 `listener` 在添加之后最多只调用一次. 如果是 `true`,`listener` 会在其被调用之后自动移除
- passive: Boolean, // 表示 `listener` 永远不会调用 `preventDefault()`. 如果 `listener` 仍然调用了这个函数, 客户端将会忽略它并抛出一个控制台警告
- }
除以上之外, 还有一个 mozSystemGroup, 我们暂时不讨论.
所以其实我们还可以在 useCapture 的基础上另外配置两个配置项.
once 表示 listener 在添加之后最多只调用一次. 如果是 true,listener 会在其被调用之后自动移除, 这跟我们在 jQuery 时代的 once 方法比较像.
passive 表示 listener 永远不会调用 preventDefault(). 如果 listener 仍然调用了这个函数, 客户端将会忽略它并抛出一个控制台警告. 具体的我们接着讨论.
passive event listener
在具体讨论
passive event listener
之前, 我们先普及一个知识点 https://developers.google.com/web/updates/2016/06/passive-event-listeners?hl=zh-cn . 大家可以自己去看看英文文档, 不过需要科学上网.
简而言之就是当我们在滚动页面的时候(通常是我们监听 touch 事件的时候), 页面其实会有一个短暂的停顿(大概 200ms), 浏览器不知道我们是否要 preventDefault, 所以它需要一个延迟来检测. 这就导致了我们的滑动显得比较卡顿.
从 Chrome 51 开始,
passive event listener
被引进了 Chrome, 我们可以通过对 addEventListener 的第三个参数设置 { passive: true } 来避免浏览器检测这个我们是否有在 touch 事件的 handler 里调用 preventDefault. 在这个时候, 如果我们依然调用了 preventDefault, 就会在控制台打印一个警告. 告诉我们这个 preventDefault 会被忽略.
当我们给 addEventListener 的第三个参数设置了{ passive: true }, 这个事件监听器就被称为 passive event listener.
从 Chrome 56 开始, 如果我们给 document 绑定 touchmove 或者 touchstart 事件的监听器, 这个 passive 是会被默认设置为 true 以提高性能, 具体 chromestatue 文档 https://www.chromestatus.com/feature/5093566007214080 . 但是我们大多数人并不知道这点, 并且依旧调用了 preventDefault. 这并不会导致什么页面崩溃级的错误, 但是这可能导致我们忽略了一个页面性能优化的点, 特别是在移动端这种更加重视性能优化的场景下.
兼容性
第三个参数是在近段时间才被调整为一组配置项, 如果我们需要兼容旧版浏览器, 我们需要写一些检测代码.
- var passiveSupported = false;
- try {
- var options = Object.defineProperty({}, "passive", {
- get: function() {
- passiveSupported = true;
- }
- });
- window.addEventListener("test", null, options);
- } catch(err) {}
这段代码为 passive 属性创建了一个带有 getter 函数的 options 对象; getter 设定了一个标识, passiveSupported, 被调用后就会把其设为 true. 那意味着如果浏览器检查 options 对象上的 passive 值时, passiveSupported 将会被设置为 true; 否则它将保持 false. 然后我们调用 addEventListener()去设置一个指定这些选项的空事件处理器, 这样如果浏览器将第三个参数认定为对象的话, 这些选项值就会被检查.
你可以利用这个方法检查 options 之中任一个值. 只需使用与上面类似的代码, 为选项设定一个 getter. 然后, 当你想实际创建一个是否支持 options 的事件侦听器时, 你可以这样做:
someElement.addEventListener("mouseup", handleMouseUp, passiveSupported ? { passive: true } : false);
我们在 someElement 这里添加了一个 mouseup. 对于第三个参数, 如果 passiveSupported 是 true, 我们传递了一个 passive 值为 true 的 options 对象; 如果相反的话, 我们知道要传递一个布尔值, 于是就传递 false 作为 useCapture 的参数.
PS: 在 vue-carousel 的那个 bug 的最开始的 PR 里, 我将 addEventListener 的 passive 设置为了 false, 但是这其实不是最优解, 最优解应该是去掉监听器里的 preventDefault, 我已经提了另一个 PR 来解决这个问题.
参考:
- MDN addEventListener https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener
- https://www.chromestatus.com/feature/5093566007214080
- google developers https://developers.google.com/web/updates/2016/06/passive-event-listeners?hl=zh-cn
来源: https://juejin.im/post/5ad804c1f265da504547fe68