『妈妈再也不用担心之』事件

501 阅读7分钟

事件

事件绑定

element.addEventListener('click', clickFunction, false)

  • type 监听事件类型的字符串
  • listener 当监听的事件类型触发时,会回调这个函数
  • options 指定有关 listener 属性的可选参数对象
    • capture: Boolean, 捕获阶段触发还是冒泡阶段触发,默认是 false,冒泡触发
    • once: Boolean, 添加之后最多只调用一次。如果为 true,lisetener 会在调用
    • passive: Boolean, 默认 false。设置为 true 时,表示 listener 永远不会调用 preventDefault()。是为了解决浏览器页面滑动流畅度而设计的。Passive 原理
  • useCapture 可选 Boolean,默认为 false。表示是在捕获阶段还是在冒泡阶段,调用该 listener

事件对象

Event对象,会被自动传递给事件处理函数,以提供额外的功能和信息

  • Event.cancelBubble 阻止冒泡的历史写法,IE6-8 使用
  • Event.returnValue 阻止默认事件的默认行为历史写法,虽收进标准,但不建议使用
  • Event.currentTarget 事件当前注册的目标的引用
  • Event.target 对事件原始目标的引用,原始目标指的是最初派发事件时指定的目标
  • Event.type 事件的类型
  • Event.eventPhase 事件传播的当前阶段 1 捕获 2 目标 3 冒泡
  • Event.isTrusted 表示事件是由浏览器(用户)发起的,还是由脚本发出的
  • Event.preventDefault() 阻止事件的默认行为
  • Event.stopPropagation() 阻止冒泡

DOM 事件流

DOM 事件产生后,从 window 对象自上而下向目标节点传递,抵达目标节点后会沿着相反方向传递 DOM 事件流包括三个阶段:捕获阶段、目标阶段、冒泡阶段

  • 捕获阶段:事件从 Widnow 对象开始触发,检查是否在捕获阶段中注册了事件,如果有,则运行它;接着到下一层中目标元素的祖先元素,并执行相同操作,依次类推,直到实际到达目标元素。
  • 目标阶段:当到达目标元素后,检查目标元素是否有在捕获阶段注册了事件,有则执行。接着检查目标元素是否在冒泡阶段注册了事件,有则执行。在同一个阶段绑定了多个事件时,与事件声明绑定顺序有关,先绑定先发生。例如 onclick 和 addEventListener 冒泡同时先后绑定在一个元素上,则先执行 onclick 再执行 addEventListener 冒泡。
  • 冒泡阶段:事件从目标元素开始触发,会把事件传递给它的父级,检查父级是否在冒泡阶段注册了事件,有则执行,接着传递到下一个祖先元素,直到到达 window 对象。

注意的是:

  • 一个事件绑定,只能执行捕获或者冒泡其中的一个阶段,但是可以在同一个元素上使用 addEventListener 绑定多个和多种事件。
  • onclick 和 attachEvent(ie10 以下的历史产物) 只能得到冒泡阶段
  • 现代浏览器中,默认事件都是在冒泡阶段进行注册
  • 有些事件是没有冒泡的,比如 onblur、onfocus、onmouseover、onmouseleave
  • 事件传递路径由浏览器、webview 等容器决定。根据 target 往 window 对象遍历就可以确定节点嵌套层级关系,从而确定此事件的传递路径
  • 事件传递要来回走两遍一是便于对事件响应时机的控制,二是浏览器历史为了兼容 IE、Netscape 两家不同的事件处理方案而诞生的。

事件委托

利用冒泡原理,可以将事件监听器设置在其父节点上,并让子节点上的事件冒泡到父节点上,而不是每个子节点单独设置监听器。减少操作 DOM,提高程序性能。

为什么要事件委托?

在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能,因为需要不断的操作 dom,那么引起浏览器重绘和回流的可能也就越多,页面交互的事件也就变的越长,这也就是为什么要减少 dom 操作的原因。每一个事件处理函数,都是一个对象,那么多一个事件处理函数,内存中就会被多占用一部分空间。如果要用事件委托,就会将所有的操作放到 js 程序里面,只对它的父级(如果只有一个父级)这一个对象进行操作,与 dom 的操作就只需要交互一次,这样就能大大的减少与 dom 的交互次数,提高性能;

那我直接把所有事件都绑定到 document 上可以吗?

如果我们直接在 document.body 上进行事件委托,可能会带来额外的问题。

图解浏览器的基本工作原理

由上可知,渲染进程几乎负责浏览器 Tab 内的所有事情,而渲染进程又包含以下线程

  • 主线程 (Main thread) Blink 内核及 V8 引擎运行的线程,如 DOM 树构建、元素布局、绘制(main-thread side)、JavaScript 执行等逻辑在该线程中执行;
  • 合成线程 (Compositor thread) 负责图像合成的线程,如绘制(impl-side),合成等逻辑在该线程中执行

合成器线程的优点在于,其工作无关主线程,合成器线程不需要等待样式计算或者 JS 执行,这就是为什么合成器相关的动画最流畅,如果某个动画涉及到布局或者绘制的调整,就会涉及到主线程的重新计算,自然会慢很多。所以只和合成相关的动画被认为是获得流畅性能的最佳选择。同时,合成器还负责处理页面的滚动,滚动的时候,合成器会更新页面的位置,并且更新页面的内容。

当一个没有绑定任何事件的页面发生滚动时,合成器可以独立于渲染主线程之外进行合成帧的的创建,保证页面的流程滚动。当页面中的某一区域绑定了 JS 事件处理程序时,合成线程会将这一区域标记为 Non-Fast Scrollable Region。这里涉及到一个专业名词「理解非快速滚动区域(non-fast scrollable region)」由于执行 JS 是主线程的工作,当页面合成时,合成器线程会标记页面中绑定有事件处理器的区域为 non-fast scrollable region ,如果存在这个标注,合成器线程会把发生在此处的事件发送给主线程,如果事件不是发生在这些区域,合成器线程则会直接合成新的帧而不用等到主线程的响应。

在开发中,我们通常会使用事件委托来简化逻辑,但是这会使整个绑定事件的区域变成 Non-Fast Scrollable Region。这意味着即使操作的是页面无绑定事件处理器的区域,每次输入时,合成器线程也需要和主线程通信并等待反馈,流畅的合成器独立处理合成帧的模式就失效了。为了减轻这种情况对滚动造成的影响,你可以传入 passive: true 选项到事件监听器中。这样写就能让浏览器既监听相关事件,又让组合器线程在等等主线程响应前构建新的组合帧。

原生实现事件委托

<ul id="myLink">
  <li id="a"> aaa </li>
  <li id="b"><span> bbb </span></li>
  <li id="c"> ccc </li>
</ul>
// 如果用户点击的是 li 里面的 span,也需要触发 fn
// element 挂载监听事件的父级元素
// eventType 事件类型
// selector 匹配指定 CSS 选择器的一个元素
// fn 执行动作
function delegate(element, eventType, selector, fn) {
    element.addEventListener(eventType, function(e) {
      if(document.querySelector(selector).contains(e.target)) {
        fn.call(e.target, e, e.target, element)
      }
    }, false)
    return element
  }