Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

事件

1. 事件循环

编程模型

JavaScript 引擎运行在主线程(单线程)上,仅负责一行行地解析和执行 js 代码,无法实现不堵塞主线程的异步任务,所以需要借助并行的编程模型

  • 【模型 1】多线程阻塞

    • 把异步任务放在另一个线程中执行,这样对于每个新的线程,都是阻塞的,但主线程不会阻塞
    • 对于这种多线程模型,需要用锁解决多线程访问共享内存的冲突问题
  • 【模型 2】事件循环

    • 让浏览器作为唯一的主线程,掌管 JavaScript 引擎的调用 和 UI 渲染
    • 对于异步任务,需要注册到对应的任务队列中,浏览器每次事件循环,都会找出满足条件的任务执行,这样就能继续往下执行 js 代码和渲染工作

对于浏览器

  • 任务队列主要分为如下两种

    • 宏任务队列

      • setTimeout、setInterval、setImmediate、网络 I/O
      • 优先级较低,可能会延迟执行
    • 微任务队列

      • Promise.then,await、MutationObserver
      • 优先级较高,需要尽快执行,所以这些事件在执行过程中,如果队列中又有新的满足条件需要执行,就会持续消费,直到队列为空
    • 还有一种任务队列

      • requestAnimationFrame
      • 它的触发时机总是和浏览器渲染保持一致
  • 两种任务的执行顺序

    • 每执行完一个宏任务,然后就要把微任务队列执行完,也就是持续消费

    • 所以下面的代码输出如下

      function test() {
        console.log("start");
        setTimeout(() => {
          console.log("children2");
          Promise.resolve().then(() => {
            console.log("children2-1");
          });
        }, 0);
        setTimeout(() => {
          console.log("children3");
          Promise.resolve().then(() => {
            console.log("children3-1");
          });
        }, 0);
        Promise.resolve().then(() => {
          console.log("children1");
        });
        console.log("end");
      }
      
      test();
      // start
      // end
      // children1
      // children2
      // children2-1
      // children3
      // children3-1
      
    • 代码输出如下

      Promise.resolve().then(() => {
        console.log("Promise1");
        setTimeout(() => {
          console.log("setTimeout2");
        }, 0);
      });
      setTimeout(() => {
        console.log("setTimeout1");
        Promise.resolve().then(() => {
          console.log("Promise2");
        });
      }, 0);
      
      // Promise1
      // setTimeout1
      // Promise2
      // setTimeout2
      

对于 Node.js

  • 事件循环基于 libuv 实现,会划分出 6 个阶段

    • timers 阶段

      • 执行 setTimeout 和 setInterval 中到期的 callback
      • 注意:在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行
    • I/O callbacks 阶段

      • 上一轮的少数 I/O callbacks 拖延到这轮完成
    • idle,prepare 阶段

    • poll 阶段

      • 内部有 poll 队列,含有 I/O callback
      • 进入该阶段时,如果 poll 队列不为空
        • 会遍历队列并同步执行,直到队列为空或达到系统限制
      • 如果 poll 队列为空
        • 同时设定了 timer
          • 判断是否有 timer 超时,有的话就回到 timer 阶段执行回调
        • 同时没有设定 timer
          • 如果有 setImmediate 回调需要执行,会直接进入到下一个 check 阶段
          • 如果没有 setImmediate 回调需要执行,就会等待一小段时间,等待新的回调加入队列并立即执行
    • check 阶段

      • 执行 setImmediate 的 callback
    • close callbacks 阶段

      • 执行 clock 事件 的 callback
    • 在 6 个阶段内部执行的任务,属于宏任务

      • 比如 setTimeout、setInterval、setImmediate、I/O callback
      • 这些都有可能因为某个阶段耗时较长而难以到期立即执行
    • 在 6 个阶段之间找机会执行的任务,属于微任务

      • 比如 process.nextTick(),Promise.then,await
      • 这些可以保证在事件循环内得到更及时的执行(因为切换阶段时就有机会执行)
  • 两种任务的执行顺序

    • node10 及以前(比较特殊)
      • 轮到 timers 阶段时,会把宏任务队列持续消费完,才会停歇,留给中间阶段执行微任务
      • 所以上面的代码执行结果会有些差异
        // start
        // end
        // children1
        // children2
        // children3
        // children2-1
        // children3-1
        
    • node11 及以后
      • 和浏览器保持一致
      • 每执行一个宏任务,然后就要把微任务队列执行完,也就是持续消费

2. 事件模型

2 种事件模型

  • DOM0事件模型 又称原始事件模型,有两种实现方式:

    //通过元素属性绑定
    <button onclick="click()">点我</button>;
    function click() {
      //do soming
    }
    
    //获取页面元素,再赋值绑定
    const btn = document.getElementById("btn");
    btn.onclick = function () {
      //do something
    };
    //解除事件
    btn.onclick = null;
    

    缺点:一个 DOM 节点只能绑定一个事件,再绑定就会覆盖

  • DOM2事件模型(W3C 制定的统一标准)

    • 新增了捕获 capture 和冒泡 bubble 的概念

    • 分为三个阶段:

      1. 事件捕获:事件从 document 向下传播到目标元素,依次检查途径节点是否绑定了监听捕获事件,如果有则执行
      2. 事件处理:事件到达目标元素,触发监听事件
      3. 事件冒泡:事件从目标元素向上传播到 document,依次检查途径节点是否绑定了监听冒泡事件,如果有则执行
    • 要注意

      • 在某种事件(比如鼠标点击或按键盘)发生后,顺序一定是,从顶层开始,先向下被捕获,然后再向上做冒泡
    • 小技巧

      • 如果事件处理绑定在 document 上,这时候第三个参数就可以决定,该事件处理是 默认落后于底层元素(第三个参数为不给或者给 false)还是 明显优先于底层元素(第三个参数参数给 true)
        • 如果默认落后的话,若底层元素和 document 同样能响应这个事件,则可以让底层元素优先响应,然后在底层元素中阻止事件继续冒泡(e.stopPropagation()),防止后续被 document 重复执行
        • 如果设置明显优先的话,若底层元素和 document 同样能响应这个事件,则可以让 document 优先响应,然后在 document 中阻止事件继续被捕获(e.stopImmediatePropagation()),防止后续被底层元素重复执行
        • 这种处理方法在 同一事件的多种处理发生冲突时 会很有用
    • 使用方式:

      btn.addEventListener('click', 回调函数,参数3)
      
      • 关键是参数 3:
        • 若为 true,则事件会在捕获阶段 先执行
        • 若为 false,则事件会在冒泡阶段 后执行
        • 由于一般不传,默认就是 false,会统一在冒泡阶段执行,这样导致捕获阶段像是完全被忽略,除非你有紧急处理需求
    • 补充

      • e.preventDefault()可以阻止事件默认的触发
      • e.stopPropogation()可以阻止事件继续向上冒泡
      • e.stopImmediatePropagation()可以阻止事件继续向下捕获
  • IE事件模型

    • 注意 IE 浏览器只支持冒泡...了解下就好

例子

  • 捕获

    <div class="t3">
      document
      <div class="t2">
        html
        <div class="t1">
          body
          <div class="t0">div</div>
        </div>
      </div>
    </div>
    
    var $t0 = document.getElementsByClassName("t0")[0];
    var $t1 = document.getElementsByClassName("t1")[0];
    var $t2 = document.getElementsByClassName("t2")[0];
    var $t3 = document.getElementsByClassName("t3")[0];
    
    $t0.addEventListener(
      "click",
      function () {
        alert("click div");
      },
      true,
    );
    
    $t1.addEventListener(
      "click",
      function () {
        alert("click body");
      },
      true,
    );
    
    $t2.addEventListener(
      "click",
      function () {
        alert("click html");
      },
      true,
    );
    
    $t3.addEventListener(
      "click",
      function () {
        alert("click document");
      },
      true,
    );
    

    根据捕获事件流模型由外向内的规则,会依次弹出: click document -> click html -> click body -> click div

  • 冒泡(把 true 改成 false)

    <div class="t3">
      document
      <div class="t2">
        html
        <div class="t1">
          body
          <div class="t0">div</div>
        </div>
      </div>
    </div>
    
    var $t0 = document.getElementsByClassName("t0")[0];
    var $t1 = document.getElementsByClassName("t1")[0];
    var $t2 = document.getElementsByClassName("t2")[0];
    var $t3 = document.getElementsByClassName("t3")[0];
    
    $t0.addEventListener(
      "click",
      function () {
        alert("click div");
      },
      false,
    );
    
    $t1.addEventListener(
      "click",
      function () {
        alert("click body");
      },
      false,
    );
    
    $t2.addEventListener(
      "click",
      function () {
        alert("click html");
      },
      false,
    );
    
    $t3.addEventListener(
      "click",
      function () {
        alert("click document");
      },
      false,
    );
    

    根据冒泡事件流模型由内向外的规则,会依次弹出: click div -> click body -> click html -> click docuement

  • 两者都有

    <div class="t3">
      document
      <div class="t2">
        html
        <div class="t1">
          body
          <div class="t0">div</div>
        </div>
      </div>
    </div>
    
    var $t0 = document.getElementsByClassName("t0")[0];
    var $t1 = document.getElementsByClassName("t1")[0];
    var $t2 = document.getElementsByClassName("t2")[0];
    var $t3 = document.getElementsByClassName("t3")[0];
    
    $t0.addEventListener(
      "click",
      function () {
        alert("click div");
      },
      false,
    );
    
    $t1.addEventListener(
      "click",
      function () {
        alert("click body");
      },
      false,
    );
    
    $t2.addEventListener(
      "click",
      function () {
        alert("click html");
      },
      true,
    );
    
    $t3.addEventListener(
      "click",
      function () {
        alert("click document");
      },
      true,
    );
    

    先潜入,再探出,所以会依次弹出: click document -> click html -> click div -> click body

3. 事件委托

  • 又叫事件代理,指的是利用事件冒泡原理,只需给外层父容器添加事件,若内层子元素有点击事件,则会冒泡到父容器上,由父容器的一个事件统一响应

  • 简单说就是:子元素委托它们的父级代为执行事件。

  • 例子

    • 音乐播放器,不能给每首歌都注册事件,就给外层列表注册一个,同时用e.target获取到真正触发事件的目标子元素
    <ul id="music">
      <li>青花瓷</li>
      <li>东风破</li>
      <li>双节棍</li>
    </ul>
    
    var $music = document.getElementById("music");
    
    $music.addEventListener(
      "click",
      function (e) {
        if (e.target.nodeName.toLowerCase() === "li") {
          // 判断目标元素target是否为li元素
          var content = e.target.innerHTML;
          console.log(content);
        }
      },
      false,
    );