New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

addEventListener 的第三个参数 #6

Open
justjavac opened this Issue Jul 4, 2017 · 6 comments

Comments

Projects
None yet
4 participants
@justjavac
Owner

justjavac commented Jul 4, 2017

原文首发在我的博客:移动 Web 滚动性能优化:Passive Event Listeners


今年的 Google I/O 已经过去一段时间了,大部分人都关注了一些新产品的发布,比如 Allo 和 Duo、Android N、Daydream、Android Studio、Firebase……还有 PWA。

网上关于 PWA 相关的文章和技术讨论已经很多了,但是关注 Passive event listeners 的文章却很少。那么 Passive event listeners 到底有多神奇呢?

如果用简单一句话来解释就是:提升页面滑动的流畅度。

addEventListener 用来在页面中监听事件,它的参数签名是这样的:

target.addEventListener(type, listener[, useCapture]);

但是如果你现在去查询 MDN 的文档却发现是这样写的:

target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);

最后一个参数 useCapture 在很久之前是必填的,后来的规范将 useCapture 变成了选填。useCapture 参数用来控制监听器是在捕获阶段执行还是在冒泡阶段执行,true 为捕获阶段,false 为冒泡阶段,变成选填后默认值为 false(冒泡阶段),因为传 true 的情况太少了。

image

此过程被称为事件传播。如果我们为每个元素都绑定事件,那么在事件冒泡过程中,子元素最先响应事件,然后依次向父元素冒泡。

在事件处理函数中,会传递 Event 对象作为参数,而这个参数最常用的 2 个方法就是 event.preventDefault()event.stopPropagation()

  • stopPropagation() 阻止事件传播

  • preventDefault() 阻止事件的默认行为

在移动网页中,我们经常使用的就是 touch 系列的事件,如:

  • touchstart

  • touchmove

  • touchend

  • touchcancel

我们可以使用如下方式绑定 touchstart 事件:

div.addEventListener("touchstart", function(e){
    // do sth.
})

由于第三个参数没有传值,那么默认就是 false,也就是这个事件在冒泡阶段被处理,如果调用了 stopPropagation()div 的父元素就无法接收这个事件。

那么如果我们调用了 preventDefault() 呢?如果你曾经给超链接 a 标签绑定过 click 事件应该就知道会发生什么了。当 a 标签点击时,它的默认行为是跳转到 href 指定的链接,如果我们调用了 preventDefault 就阻止了 a 标签点击事件的默认行为。(如果你使用 jQuery 通过 return false 可以阻止事件默认行为,但是深记 You Might Not Need jQuery )

如果我们在 touchstart 事件调用 preventDefault 会怎样呢?这时页面会禁止,不会滚动或缩放。那么问题来了:浏览器无法预先知道一个监听器会不会调用 preventDefault(),它需要等监听器执行完后,再去执行默认行为,而监听器执行是要耗时的,这样就会导致页面卡顿。

这段翻译的太专业了,你可以这么理解:当你触摸滑动页面时,页面应该跟随手指一起滚动。而此时你绑定了一个 touchstart 事件,你的事件大概执行 200 毫秒。这时浏览器就犯迷糊了:如果你在事件绑定函数中调用了 preventDefault,那么页面就不应该滚动,如果你没有调用 preventDefault,页面就需要滚动。但是你到底调用了还是没有调用,浏览器不知道。只能先执行你的函数,等 200 毫秒后,绑定事件执行完了,浏览器才知道,“哦,原来你没有阻止默认行为,好的,我马上滚”。此时,页面开始滚。

而且 Chrome 做了统计:

For instance, in Chrome for Android 80% of the touch events that block
scrolling never actually prevent it. 10% of these events add more than
100ms of delay to the start of scrolling, and a catastrophic delay of
at least 500ms occurs in 1% of scrolls.

在 Android 版 Chrome 浏览器的 touch 事件监听器的页面中,80% 的页面都不会调用 preventDefault 函数来阻止事件的默认行为。在滑动流畅度上,有 10% 的页面增加至少 100ms 的延迟,1% 的页面甚至增加 500ms 以上的延迟。

也就是说,当浏览器等待执行事件的默认行为时,大部分情况是白等了。如果 Web 开发者能够提前告诉浏览器:“我不调用 preventDefault 函数来阻止事件事件行为”,那么浏览器就能快速生成事件,从而提升页面性能。

Chrome官方有个视频测试:https://www.youtube.com/watch?v=NPM6172J22g (需科学上网)

passive 就是为此而生的。在 WICG 的 demo 中提到,即使滚动事件里面写一个死循环,浏览器也能够正常处理页面的滑动。

在最新的 DOM 规范中,事件绑定函数的第三个参数变成了对象:

target.addEventListener(type, listener[, options]);

我们可以通过传递 passivetrue 来明确告诉浏览器,事件处理程序不会调用 preventDefault 来阻止默认滑动行为。

在 Chrome 浏览器中,如果发现耗时超过 100 毫秒的非 passive 的监听器,会在 DevTools 里面警告你加上 {passive: true}

Chrome 51 和 Firefox 49 已经支持 passive 属性。如果浏览器不支持,已经有人做了非常巧妙地 polyfill:

// Test via a getter in the options object to see 
// if the passive property is accessed
var supportsPassive = false;
try {
  var opts = Object.defineProperty({}, 'passive', {
    get: function() {
      supportsPassive = true;
    }
  });
  window.addEventListener("test", null, opts);
} catch (e) {}
   
// Use our detect's results. 
// passive applied if supported, capture will be false either way.
elem.addEventListener(
  'touchstart',
  fn,
  supportsPassive ? { passive: true } : false
); 

这段代码值得细读,用的太巧妙了,简直炫酷到没朋友。

不过,遗憾的是各大主流框架都还未提供对 passive 的原生支持:

React 框架番号为 #6436 的 issue 从今年 4 月 8 号就开始讨论,官方家加的 label 是 “big picture”。

Support Passive Event Listeners · Issue #6436 · facebook/react

本来我觉得 Angular 应该会提供对 passive 的原生支持,但是我去搜了 issue。编号为 #8866 的 issue 残忍的告诉我 Angular 也正在讨论此事,而且时间比 React 还晚,5月27号。而 label 是 “state: Needs Design”。

[feat] Take advantage of passive event listeners · Issue #8866 · angular/angular

最不应该报什么希望的就是 jQuery 了,毕竟对于一个连事件捕获都不支持的框架来说,很难想象它会支持 passive。

去看了它的 issue,果然是前端最流行的框架,在今年1月17号就有开发者提了相关 issue 了。和 React Angular 不同,这个 issue 并不是 jQuery 开发成员提的。label 是 “Event”、 “Feature”、 “Web Standards”。

从 label 可以看出来对这个特性的重视,但是再看看 milestone 就未免有些心寒——4.0.0。

如果没觉得心寒,可以继续点击进去看看 4.0.0 的开发进度和发布计划:

什么时候发布呢?No due date

开发进度如何呢?0% complete

我之前开发过一个 Chrome 插件 ChromeSnifferPlus,可以检测当前页面使用了哪些框架,以及框架的版本号等信息。年初的时候我曾分析了一次统计情况,jQuery 版本使用最多的网站是 1.4.* 和 1.7.*。

最后,也不要悲观,上面只是说了这些框架并没有提供对 passive 的原生支持,并不代表我们就不可以使用了。比如 React 的很多 scroll 组件,都使用了 passive 来提升滚动性能。

@yxuchen

This comment has been minimized.

Show comment
Hide comment
@yxuchen

yxuchen commented Jun 11, 2018

m

@scutHuqiang

This comment has been minimized.

Show comment
Hide comment
@scutHuqiang

scutHuqiang Jul 10, 2018

请问一下,passive那个pollyfil怎么理解啊?

scutHuqiang commented Jul 10, 2018

请问一下,passive那个pollyfil怎么理解啊?

@justjavac

This comment has been minimized.

Show comment
Hide comment
@justjavac

justjavac Jul 10, 2018

Owner

@scutHuqiang 如果浏览器支持,那么 supportsPassive = true; 就会执行;如果浏览器不支持,则这段代码就不执行。

Owner

justjavac commented Jul 10, 2018

@scutHuqiang 如果浏览器支持,那么 supportsPassive = true; 就会执行;如果浏览器不支持,则这段代码就不执行。

@scutHuqiang

This comment has been minimized.

Show comment
Hide comment
@scutHuqiang

scutHuqiang Jul 11, 2018

感谢回复,是不是说当调用window.addEventListener("test", null, opts)这个方法的时候,如果浏览器支持passive,就会去查看opts里面是否有passive这个属性,此时get回调就会执行,如果不支持,就不会去查看是否有passive这个属性?

scutHuqiang commented Jul 11, 2018

感谢回复,是不是说当调用window.addEventListener("test", null, opts)这个方法的时候,如果浏览器支持passive,就会去查看opts里面是否有passive这个属性,此时get回调就会执行,如果不支持,就不会去查看是否有passive这个属性?

@justjavac

This comment has been minimized.

Show comment
Hide comment
@justjavac

justjavac Jul 11, 2018

Owner

是的

Owner

justjavac commented Jul 11, 2018

是的

@linjiajian999

This comment has been minimized.

Show comment
Hide comment
@linjiajian999

linjiajian999 Jul 20, 2018

vuejs .passive修饰符 已经支持passive选项了

linjiajian999 commented Jul 20, 2018

vuejs .passive修饰符 已经支持passive选项了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment