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. Mysql经典sql语句大全

    DDL-数据定义语言(Create,Alter,Drop,DECLARE) DML-数据操纵语言(Select,Delete,Update,Insert) DCL-数据控制语言(GRANT,REVOK ...

  2. java基础基础总结----- 常用了解java(二)

    一.Java的发展历史 1998年,Java 2 Platform J2SE.J2ME.J2EE 2005年,Java10周年,重新命名: J2SE → Java SE → Standard Edit ...

  3. Angular组件生命周期钩子

    Angular会按以下顺序依次调用以下钩子(8个): ngOnChanges ngOnInit ngDoCheck ngAfterContentInit ngAfterContentChecked n ...

  4. Nginx插件之openresty反向代理和日志滚动配置案例

    Nginx插件之openresty反向代理和日志滚动配置案例 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.openresty介绍 1>.Nginx介绍 Nginx是一款 ...

  5. Swift学习笔记4

    1.延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性.在属性声明前使用lazy来标示一个延迟存储属性. 必须将延迟存储属性声明成变量(使用var关键字),因为属性的初始值可能在实例构造完成之 ...

  6. css颜色模式hsla和rgba

    在CSS3中可以使用RGBA和HSLA两种色彩模式,这两个都可以用来设置颜色以及指定透明度. rgba指的是:红色.绿色.蓝色.Alpha透明度(Red-Green-Blue-Alpha)前三个值取值 ...

  7. spring boot(九):Spring Boot中Redis的使用

    Redis实战代码 1.引入 spring-boot-starter-redis <dependency> <groupId>org.springframework.boot& ...

  8. FastReport"Text"对象中的HTML标签介绍以及使用

    "Text"对象可以理解一些简单的HTML标签.标签可位于对象中的文本.在默认情况下,标签是禁用的,要启用这些HTML标签,可选择对象上下文菜单中的“Allow HTML tags ...

  9. C++函数返回局部变量

    函数不能返回指向栈内存的指针 原因:返回值是拷贝值,局部变量的作用域为函数内部,函数执行结束,栈上的局部变量会销毁,内存释放. 可返回的局部变量: 1. 返回局部变量本身 int sum(int a, ...

  10. Linux 文件日志筛选操作

    统计查看文件以及筛选日志 1.*.log 日志文件中 统计独立ip的个数: awk '{print $1}' test.log | sort | uniq | wc -l 2.#查询访问最多的前10个 ...