你真的会用addEventListener吗?vue
没错,的确很标题党,可是我最近发现个人一个朋友(谁说是个人!)真的不太会用addEventListener
。git
我最近参与了一个开源项目,vue-carousel,Vue生态圈里的一个轮播组件,由加拿大的一个电商公司SSENSE开源的。而后恰好有人提了一个issuegithub
Unable to preventDefault inside passive event listener due to target being treated as passiveweb
也许有人在开发移动端应用的时候遇到过这个问题,我来为你们复现一下这个场景:chrome
当咱们给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
的第三个参数。markdown
当咱们使用addEventListener
的时候,咱们通常的写法是如下:ide
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
以前,咱们先普及一个知识点。你们能够本身去看看英文文档,不过须要科学上网。
简而言之就是当咱们在滚动页面的时候(一般是咱们监听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 文档。可是咱们大多数人并不知道这点,而且依旧调用了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来解决这个问题。
参考: