事件传播的 3 个阶段

  1. 捕获阶段(Capturing phase)—— 事件(从 Window)向下走近元素。
  2. 目标阶段(Target phase)—— 事件到达目标元素。
  3. 冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡。

冒泡

  • 参考冒泡和捕获
  • 当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序。
  • 引发事件的那个嵌套层级最深的元素被称为目标元素,可以通过event.target访问
  • 用于停止冒泡的方法
    1. event.stopPropagation(): 停止向上移动,但是当前元素上的其他处理程序都会继续运行
    2. event.stopImmediatePropagation() : 停止冒泡,且其他处理程序不会被执行。
  • JavaScript 中那些不会冒泡的事件 - 知乎
    1. scroll
    2. blur & focus
    3. media事件
    4. mouseLeave & mouseEnter
    5. 注意:mouseout/mouseover 会触发冒泡

preventDefault

window.addEventListener("beforeunload", (event) => {
  // Cancel the event as stated by the standard.
  event.preventDefault();
  // Chrome requires returnValue to be set.
  event.returnValue = "";
});

addEventListener

语法

addEventListener(type, listener)
addEventListener(type, listener, options)
addEventListener(type, listener, useCapture) //旧版本的 DOM 的规定

options

  • capture:boolean
  • once:boolean
  • passive:boolean True -> preventDefault() 不会被调用
  • signal

注意

  • 处理过程中 this 的值的问题 | MDN
  • 同一个元素节点注册了多个相同的 EventListener,那么重复的实例会被抛弃。这么做不会让得 EventListener 被重复调用,也不需要用 removeEventListener 手动清除多余的 EventListener,因为重复的都被自动抛弃了。
  • 只是针对于命名函数。对于匿名函数,浏览器会将其看做不同的 EventListener

参考


removeEventListener

  • 如果同一个事件监听器分别为“事件捕获(capture 为 true)”和“事件冒泡(capture 为 false)”注册了一次,这两个版本的监听器需要分别移除。移除捕获监听器不会影响非捕获版本的相同监听器,反之亦然。
  • options只有 capture 配置影响 removeEventListener()
  • 参考EventTarget: removeEventListener() method - Web APIs | MDN

事件委托

概念

  • 事件处理模式之一,也称事件代理
  • 对于许多以类似方式处理的元素,不必为每个元素分配一个处理程序,而是将单个处理程序放在它们的共同祖先上
  • 在处理程序中,我们获取event.target以查看事件实际发生的位置并进行处理。

优点:

  1. 减少事件注册,提升性能
  2. 简化了dom节点更新时,相应事件的更新

缺点:

  1. 基于冒泡,对于不冒泡的事件不支持
  2. 层级过多,冒泡过程中,可能会被某层阻止掉。
  3. 理论上委托会导致浏览器频繁调用处理函数,虽然很可能不需要处理。所以建议就近委托,比如在table上代理td,而不是在document上代理td

React中的事件委托

  • React 借鉴事件委托的方式将大部分事件委托给了 Document 对象
  • 事件委托需要区分捕获和冒泡,有些事件由于没有冒泡过程,只能在捕获阶段进行事件委托
  • 没有进行委托的事件是Form事件和Media事件,原因是这些事件针对特性类型元素,委托意义不大,React 将其直接注册到了目标对象
  • React 中的事件分为 3 类。
    1. DiscreteEvent:click,blur,focus,submit,tuchStart 等。优先级最低。
    2. UserBlockingEvent:touchMove,mouseMove,scroll,drag,dragOver 等。这些事件会阻塞用户的交互,优先级次之
    3. ContinuousEvent:load,error,loadStart,abort,animationend 等。优先级最高,不会被打断。

示例

  • html
    <ul>
      <li>Exercise</li>
      <li>Write code</li>
      <li>Play music</li>
      <li>Relax</li>
    </ul>
    
  • css
    const list = document.querySelector("ul");
    list.addEventListener(
      "click",
      (ev) => {
        if (ev.target.tagName === "LI") {
          ev.target.classList.toggle("done");
        }
      },
      false,
    );
    

参考


setTimeout

  • 参考setTimeout() 全局函数 - Web API 接口参考 | MDN
  • 参数:
    var timeoutID = setTimeout(func, [delay, arg1, arg2, ...]);
    // timeoutID 可用来作为 clearTimeout() 的参数来清除对应的定时器
    // arg1, ..., argN 可选  当计时结束的时候,将被传递给 func 函数的附加参数。
    
  • func应是对函数的引用,示例:
function sayHi() {
  alert('Hello');
}
// 正确
setTimeout(sayHi, 1000);
// 错的!
setTimeout(sayHi(), 1000); //这样传入的是sayHi函数的执行结果:undefined
  • delay默认为0,代表了消息被实际加入到队列的最小延迟时间。如果队列中没有其他消息并且栈为空,在这段延迟时间过去之后,消息会被马上处理。
  • 在默认情况下,在 setTimeout() 内部,this 关键字将被设置为 globalThis,在浏览器中它是 window 对象
  • setTimeout为啥不准(线程间通信怎么都会有延迟的)
  • 定时器原理setTimeout/setInterval(以前翻过v8的源码,本质上都是二叉堆,与react里的scheduler的实现有异曲同工之处)

setInterval

  • 参数:

    var intervalID = setInterval(func, [delay, arg1, arg2, ...]);
    // intervalID 可用来作为 clearInterval() 的参数来清除对应的定时器
    // arg1, ..., argN 可选  当计时结束的时候,将被传递给 func 函数的附加参数。
    
  • 每间隔给定的时间周期性执行,时间开始计算的位置是调用内部方法的那一刻,因此第一次方法结束到第二次开始之间的时间间隔其实是小于delay的

  • 如果setInterval的内部函数执行耗时大于设定的时间间隔,会发生什么?

    • a: JavaScript 引擎会等待 func 执行完成,然后检查调度程序,如果时间到了,则 立即 再次执行它。极端情况下,如果函数每次执行时间都超过 delay 设置的时间,那么每次调用之间将完全没有停顿。
    • 解决办法: 嵌套的 setTimeout
      // setInterval
      let i = 1;
      setInterval(function(){
        func(i++);
      },100);
      // 嵌套的setTimeout
      let i = 1;
      setTimeout(function run(){
        func(i++);
        setTimeout(run,100);
      },100)
      
  • 参考:


事件循环

三种数据结构:

  1. stack: 函数调用形成了一个由若干帧组成的栈
  2. heap: 对象被分配在堆中
  3. queue: 一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数

两种任务

  • JavaScript 的异步任务根据事件分类分为两种:宏任务(MacroTask)和微任务(MicroTask)

  • 宏任务

    • I/O(Mouse Events、Keyboard Events、Network Events)
    • setTimeout、setInterval、setImmediate
    • UI Rendering(HTML Parsing)
    • MessageChannel
    • JavaScript Run
  • 微任务

    • DOM mutations
    • Promises
  • 优先级

    • 宏任务的优先级高于微任务
    • 每个宏任务执行完毕后都必须将当前的微任务队列清空
  • 如何安排(schedule)一个新的宏任务:

    • 可使用零延迟的 setTimeout(f)
    • 好处
      1. 将繁重的计算任务拆分成多个部分,以使浏览器能够对用户事件作出反应,并在任务的各部分之间显示任务进度。
      2. 也被用于在事件处理程序中,将一个行为(action)安排(schedule)在事件被完全处理(冒泡完成)后。
  • 如何安排一个新的 微任务:

    • 可以使用 queueMicrotask 来在保持环境状态一致的情况下,异步地执行一个函数。
    • 如果我们想要异步执行(在当前代码之后)一个函数,但是要在更改被渲染或新事件被处理之前执行,那么我们可以使用 queueMicrotask 来对其进行安排(schedule)。

浏览器中的事件循环和Node.js中事件循环的区别

  • 浏览器是一个宏任务+一个微任务队列
  • node是一个宏任务队列+一个微任务队列

参考


面试问题

  1. 实现一个事件处理对象,包括绑定,取消,执行功能 思路:构造函数里面初始化一个空对象,存储事件名。绑定,取消(对应置空)就在这个数组里面操作。执行先判断是否存在,然后调用apply函数执行回调。