passive 的事件监听器

很久以前,addEventListener() 的参数约定是这样的:

addEventListener(type, listener, useCapture)

后来,最后一个参数,也就是控制监听器是在捕获阶段执行还是在冒泡阶段执行的 useCapture 参数,变成了可选参数(传 true 的情况太少了),成了:

addEventListener(type, listener[, useCapture ])

去年年底,DOM 规范做了修订:addEventListener() 的第三个参数可以是个对象值了,也就是说第三个参数现在可以是两种类型的值了:

addEventListener(type, listener[, useCapture ])
addEventListener(type, listener[, options ])

这个修订是为了扩展新的选项,从而自定义更多的行为,目前规范中 options 对象可用的属性有三个:

addEventListener(type, listener, {
capture: false,
passive: false,
once: false
})

三个属性都是布尔类型的开关,默认值都为 false。其中 capture 属性等价于以前的 useCapture 参数;once 属性就是表明该监听器是一次性的,执行一次后就被自动 removeEventListener 掉,还没有浏览器实现它;passive 属性是本文的主角,Firefox 和 Chrome 已经实现,先看个 Chrome 官方的视频介绍(单击播放):

很多移动端的页面都会监听 touchstart 等 touch 事件,像这样:

document.addEventListener("touchstart", function(e){
... // 浏览器不知道这里会不会有 e.preventDefault()
})

由于 touchstart 事件对象的 cancelable 属性为 true,也就是说它的默认行为可以被监听器通过 preventDefault() 方法阻止,那它的默认行为是什么呢,通常来说就是滚动当前页面(还可能是缩放页面),如果它的默认行为被阻止了,页面就必须静止不动。但浏览器无法预先知道一个监听器会不会调用 preventDefault(),它能做的只有等监听器执行完后再去执行默认行为,而监听器执行是要耗时的,有些甚至耗时很明显,这样就会导致页面卡顿。视频里也说了,即便监听器是个空函数,也会产生一定的卡顿,毕竟空函数的执行也会耗时。

视频里还说了,有 80% 的滚动事件监听器是不会阻止默认行为的,也就是说大部分情况下,浏览器是白等了。所以,passive 监听器诞生了,passive 的意思是“顺从的”,表示它不会对事件的默认行为说 no,浏览器知道了一个监听器是 passive 的,它就可以在两个线程里同时执行监听器中的 JavaScript 代码和浏览器的默认行为了。

下面是在 Chrome for Android 上滚动 cnn.com 页面的对比视频,右边在注册 touchstart 事件时添加了 {passive: true} 选项,左边没有,可以看到,右边的顺畅多了。

假如在一个 passive 的监听器里执行了 preventDefault() 会怎么样?

假如有人不小心在 passive 的监听器里调用了 preventDefault(),也无妨,因为 preventDefault() 不会产生任何效果。这里我用自定义事件演示一下这种情况:

let event = new Event("foo", {  // 创建一个 type 为 foo 的事件对象,可以被阻止默认行为
"cancelable": true
}) document.addEventListener("foo", function(event) { // 在 document 上绑定 foo 事件的监听函数
console.log(event.defaultPrevented) // false
event.preventDefault()
console.log(event.defaultPrevented) // 还是 false,preventDefault() 无效
}, {
passive: true
}) document.dispatchEvent(event) // 派发自定义事件

同时,浏览器的开发者工具也会发出警告:

Chrome 下:

Firefox 下:

开发者工具的支持

除了上面在 passive 的监听器里调用 preventDefault() 会发出警告外,Chrome 的开发者工具还会:

1. 发现耗时超过 100 毫秒的非 passive 的监听器,警告你加上 {passive: true}:

2. 给监听器对象增加 passive 属性,监听器对象在普通页面中是获取不到的,可以在 Event Listeners 面板中和通过调用 getEventListeners() Command Line API 获取到:

Firefox 的开发者工具目前还没有这些。

现在该如何调用 removeEventListener?

以前,在第三个参数是布尔值的时候,addEventListener("foo", listener, true) 添加的监听器,必须用 removeEventListener("foo", listener, true) 才能删除掉。因为这个监听器也有可能还注册在了冒泡阶段,那样的话,同一个监听器实际上对应着两个监听器对象(通过 getEventListeners() 可看到)。

那现在 addEventListener("foo", listener, {passive: true}) 添加的监听器该如何删除呢?答案是 removeEventListener("foo", listener) 就可以了,passive 可以省略,原因是:在浏览器内部,用来存储监听器的 map 的 key 是由事件类型,监听器函数,是否捕获这三者组成的,passive 和 once 不在其中,理由显而易见,一个监听器同时是 passive 和非 passive(以及同时是 once 和非 once)是说不通的,如果你添加了两者,那么后添加的不算,浏览器会认为添加过了:

addEventListener("foo", listener, {passive: true})
addEventListener("foo", listener, {passive: false}) // 这句不算 addEventListener("bar", listener, {passive: false})
addEventListener("bar", listener, {passive: true}) // 这句不算

所以说在 removeEventListener 的时候永远不需写 passive 和 once,但 capture 可能要:

addEventListener("foo", listener, {capture: true})
removeEventListener("foo", listener, {capture: true}) // {capture: true} 必须加,当然 {capture: true} 换成 true 也可以

passive 不能保证什么

passive 监听器能保证的只有一点,那就是调用 preventDefault() 无效,至于浏览器对默认行为卡顿的优化,那是浏览器的事情,是在规范要求之外的。鉴于这个新特性本来就是为解决滚动和触摸事件的卡顿而发明的,目前 Chrome 和 Firefox 支持优化的事件类型也仅限这类事件,比如 touchstart,touchmove,wheel 等事件,具体的事件列表我无法提供,也许得研读源码才行。

但我可以列举几个浏览器不优化的事件类型,还附带 demo:

除了这三种事件类型外,所有 Cancelable 为 true 的事件类型理论上都是可以有这种优化的, 可以看看这个UI 事件类型列表,还有这个触摸事件类型列表,注意 Cancelable 列。

我咨询了 Chrome 工程师,有没有优化滚动和触摸事件类型之外事件类型的计划,答复是目前没有:

Firefox 的 APZ 优化

在 passive 规范之前,Firefox 就已经有了自己对滚动触摸行为卡顿问题的优化,其中有个关键做法是,不尊重 preventDefault():如果在一定时间之内没有调用 preventDefault(),那 Firefox 就假定你不会阻止默认滚动了,比如执行下面这句后,页面会无法滚动:

addEventListener("wheel", function (e) {
e.preventDefault()
})

但执行这句后照样能滚动:

addEventListener("wheel", function (e) {
sleep(300)
e.preventDefault() // 这句在 Firefox 中无效
})

这篇博客讲了 APZ 优化:Smoother scrolling in Firefox 46 with APZ

特性检测

下面是从 Modernizr 里复制的检测脚本:

var supportsPassiveOption = false;
try {
var opts = Object.defineProperty({}, 'passive', {
get: function() {
supportsPassiveOption = true;
}
});
window.addEventListener('test', null, opts);
} catch (e) {}

可以这么用:

if (supportsPassiveOption) {
document.addEventListener("foo", listener, {passive: true}) // 旧浏览器里第三参数会被自动转成 true,不是我们想要的
}
else {
document.addEventListener("foo", listener)
}

要 passive 都得 passive

对于在同一个 DOM 对象身上添加的同一类型事件的监听器,只要有一个不是 passive 的,那浏览器就无法优化。

passive 的事件监听器(转载)的更多相关文章

  1. passive 的事件监听器

    很久以前,addEventListener() 的参数约定是这样的: addEventListener(type, listener, useCapture) 后来,最后一个参数,也就是控制监听器是在 ...

  2. js事件监听器用法实例详解

    这篇文章主要介绍了js事件监听器用法,以实例形式较为详细的分析了javascript事件监听器使用注意事项与相关技巧,需要的朋友可以参考下本文实例讲述了js事件监听器用法.分享给大家供大家参考.具体分 ...

  3. Java基础之处理事件——实现低级事件监听器(Sketcher 2 implementing a low-level listener)

    控制台程序. 定义事件监听器的类必须实现监听器接口.所有的事件监听器接口都扩展了java.util.EventListener接口.这个接口没有声明任何方法,仅仅用于表示监听器对象.使用EventLi ...

  4. [原创]java WEB学习笔记48:其他的Servlet 监听器:域对象中属性的变更的事件监听器 (3 个),感知 Session 绑定的事件监听器(2个)

    本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ...

  5. Android编程: 界面组成、事件监听器

    学习知识:界面组成.事件监听器 ====界面组成==== 1.用户界面的基本组件叫做View,都是继承android.view.View类,Android里面预定义很多基本的界面组件,比如 Butto ...

  6. Hibernate拦截器(Interceptor)与事件监听器(Listener)

    拦截器(Intercept):与Struts2的拦截器机制基本一样,都是一个操作穿过一层层拦截器,每穿过一个拦截器就会触发相应拦截器的事件做预处理或善后处理. 监听器(Listener):其实功能与拦 ...

  7. Android事件监听器Event Listener

    在 Android 中,我们可以通过事件处理使UI与用户互动(UI Events). UI的用户事件处理,即View处理用户的操作,在应用程序中几乎不可避免.View是重要的类,它是与用户互动的前线: ...

  8. Servlet事件监听器

    监听器就是一个实现特定接口的普通java程序,这个程序专门用于监听另一个java对象的方法调用或属性改变,当被监听对象发生上述事件后,监听器某个方法将立即被执行. 面试题:请描述一下java事件监听机 ...

  9. 第13章 Swing程序设计----常用事件监听器

    组件本身并不带有任何功能.这时需要为这些组件添加特定事件监听器. Swing中常用的两个事件监听器,即动作事件监听器和焦点事件监听器.

随机推荐

  1. 2018acm-icpc宁夏邀请赛后记

    由于要准备期末考试的缘故,时隔一个月之后我才想起来还有一场比赛没有总结. 大概是出去过的最远的地方了,宁夏理工学院离杭州有两千多千米. 不得不说感到了主办方满满的诚意,虽然是第一次办比赛,但是第一天组 ...

  2. bottle框架剖析

    bottle框架剖析 使用 源码分析 一.使用 大致有以下几部分 quick start request routing generate contents request Data template ...

  3. CentOS Linux release 7.3破解密码详解

    CentOS Linux release 7.3破解密码详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 公司最近接了一个项目,拿到客户现有的源代码,但是服务器用户密码并不知情, ...

  4. 约束Constraints--主键约束、外键约束、唯一约束、检查约束、默认约束、NOT NULL约束、列约束与表约束、创建约束、删除约束

    约束   Including Constraints 以下内容转自:https://www.cnblogs.com/wcl2017/p/7043939.html和http://blog.csdn.ne ...

  5. 标准遗传算法(实数编码 python实现)模拟二进制交叉SBX 多项式变异

    代码地址: https://github.com/guojun007/real_sga 本部分是采用实数编码的标准遗传算法,整体流程与上一篇二进制编码的基本一致, 主要区别在于本部分的交叉操作为模拟二 ...

  6. Linux系统加固

    iptables 初始化 > iptables -F #清空所有的链 > iptables -X #清空所有自定义的链 关掉全部端口 > iptables -P INPUT DROP ...

  7. IntelliJ IDEA AndroidStudio SVN无法使用

    1.Cann't Run Program "SVN" 把勾都去掉,结果没有任何反应.2.重新安装TotoriseSVN,设置Svn.exe路径,主要不要勾选Enable Inter ...

  8. JavaSE回顾及巩固的自学之路(四)——————方法和数组,面向对象

    今天是2018.03.31,emmmmmm.好像距离上一次写Javase回顾总结已经好久好久过去,差一点就以为要停更了,哈哈哈.        其实呢,最近是真的好忙(额,这段时间觉得自己一直在学习) ...

  9. 计算机网络之互联网|因特网|万维网|HTTP|HTML之间的关系辨析

    本博文基于知乎"Web 是什么意思?"一问而引起.(本文均属于博主从知乎上自身所答搬运而至). 如无特殊声明,括号()内以分号分隔的名词均等效. 本文如无特殊引用声明,则所有内容版 ...

  10. luogu P3960 列队

    传送门 因为\(Splay\)可以\(O(logn)\)维护区间,所以直接对每一行维护第一个元素到倒数第二个元素的\(Splay\),最后一列维护一个\(Splay\),每次把选出来的点删掉,然后把那 ...