浏览器事件

1.浏览器事件循环

事件循环介绍:

  1. 事件循环的创建时间是在tab页面创建或者web worker创建时创建。

  2. 事件循环的目的是不阻塞主线程JavaScript的运行,提供异步回调机制。

  3. 事件循环的工作机制

  • (持续循环) 在所有异步事件结束之前事件循环一直执行一个类似于while(true)的循环,不停止。

  • (宏任务和微任务队列) 事件循环会维护一个task queue(宏任务队列)和一个microtask queue(微任务队列)。宏任务队列中有多个子队列,比如定时器回调函数子队列,网络IO回调函数子队列等待,虽然叫子队列,但是它们实际上是JavaScript中的集合set,JS中的集合是有顺序的,是元素添加顺序,每次执行回调函数不是从队列里出队,而是从集合中取出这个元素并删除----WhatWG标准。微任务只有一个队列。

Task queues are sets, not queues, because step one of the event loop processing model grabs the first runnable task from the chosen queue, instead of dequeuing the first task.

  • (任务队列执行顺序) 事件循环每次先执行遍历宏任务队列,每执行完一个宏任务子队列后立即遍历微任务队列,执行完微任务队列中的所有回调函数,执行完微任务后执行渲染操作----参考WhatWG标准。由于先执行的是JavaScript脚本,是宏任务,执行完毕后就不会再次执行,因此从表现上来看,除了第一次事件循环先执行宏任务之外,其它时间都是先执行微任务再执行宏任务。
  1. Let taskQueue be one of the event loop’s task queues, chosen in an implementation-defined manner, with the constraint that the chosen task queue must contain at least one runnable task. If there is no such task queue, then jump to the microtasks step below.
  2. Let oldestTask be the first runnable task in taskQueue, and remove it from taskQueue.
  3. Set the event loop’s currently running task to oldestTask.
  4. Let taskStartTime be the current high resolution time.
  5. Perform oldestTask’s steps.
  6. Set the event loop’s currently running task back to null.
  7. Microtasks: Perform a microtask checkpoint.
  8. Update the rendering
    在这里插入图片描述
  • (任务队列的实现) 在"浏览器进程与线程"中提到了浏览器下两种渲染进程中的线程的说法,其中比较官方的说法(来自谷歌开发者社区)并没有提到事件循环和定时器处理工作占用线程的情况。在WhatWG标准中提到,事件循环不一定要由一个线程实现,在浏览器中可以通过一个线程管理多个tab页的事件循环。

Event loops do not necessarily correspond to implementation threads. For example, multiple window event loops could be cooperatively scheduled in a single thread.

  1. 浏览器宏任务和微任务
    • 宏任务:JavaScript脚本,定时器函数setTimeout和setInterval(setImmediate仅IE10和Node支持),IO操作,UI渲染也可以当做宏任务
    • 微任务:Promise.then, catch, finally方法,MutationObserver.observe方法,queueMicrotask方法,(Object.observe方法,已经被废弃,可以使用proxy。目前在mdn,JavaScript高级程序设计2021版,JavaScript权威指南2021版均无该方法的介绍)

2.Node中的事件循环

事件循环介绍:

  1. 事件循环的创建时间Node进程/线程开始运行时创建的

  2. 事件循环的目的是不阻塞主线程JavaScript的运行,提供异步回调机制。

  3. 事件循环的工作机制

  • (持续循环)

  • (宏任务和微任务队列) 在Node中使用宏任务和微任务的概念并不恰当,但是意思上对的。Node事件循环是有多个阶段的,这个阶段就对应着浏览器事件循环中按照顺序执行多个任务。在每个阶段中Node都有一个队列存储该阶段对应的回调函数,Node也有一个类似浏览器事件循环的微任务队列。Node有如下阶段timers(定时器阶段)->pending callbacks(等待回调)->idle,prepare->poll(IO轮询)->check(检查)->close callbacks(关闭操作回调)

  • (执行顺序)

  1. 执行微任务。首先Node到达timers阶段,定时器观察者查看定时器容器是否有过期的定时器,有的话将其回调函数加入回调函数队列并执行—朴灵<<深入浅出NodeJS>>。
  2. 执行微任务。再到达pending callbacks阶段,执行被推迟到下一个循环执行的IO回调,例如TCP连接发生错误。
  3. 执行微任务。再到达idle,prepare阶段,是系统内部使用。
  4. 执行微任务。再到达poll阶段,开始执行IO轮询(poll轮询可以参考UNIX网络编程下的5中IO模型),该阶段会发生阻塞。事件循环做出了优化,当当前阶段的回调函数队列为空时会做出判断。如果有setImmediate调度,那么直接进入check阶段。如果没有的话就像定时器观察者检查当前是否有过期的定时器,如果有那么就进入timers阶段执行回调函数。如果既没有setImmediate也没有setTimeout过期,那么就在该阶段阻塞,等待IO轮询。
  5. 执行微任务。再到达check阶段,开始执行setImmediate的回调。
  6. 执行微任务。再到达close callbacks阶段。开始执行一些套接字的关闭的回调函数。
    在这里插入图片描述
  1. Node宏任务和微任务
    • 宏任务:定时器函数setTimeout和setInterval,setImmediate,IO操作
    • 微任务:Promise.then, catch, finally方法,queueMicrotask方法,process.nextTick方法 (注意,在nodejs的微任务中是process.nextTick()先执行)

3.事件循环中宏任务和微任务介绍

1.MutationObserver

介绍:

  1. 监听DOM元素的变化,可以监听到自身属性变化,子节点或子树的变化。

  2. 使用需要先创建一个MutationObserver对象,参数是回调函数。

  3. 创建的MutationObserver对象调用observe方法监听感兴趣的DOM元素,该对象可以监听多个DOM元素。

测试内容:

  1. MutationObserver监听对象进行多次修改,修改后是将对应多次修改的多个回调函数放入微任务队列,还是只放入一个回调函数记录多次修改。

  2. 已经在<<JavaScript高级程序设计>>上了解了第一个内容的答案是放入一个回调函数,那么如果MutationObserver监听对象在回调函数中进行修改,修改后是会在当前微任务中加入新的回调函数,还是只是更新当前回调函数的参数mutationRecords。

  3. 在第二个问题基础上假设会在当前微任务队列中加入一个新的回调函数,那么这个回调函数是会在本次任务继续执行,还是到下一个任务中,执行完宏任务再执行微任务队列中的该回调函数。

设计测试方法:

  1. 针对第一个问题: 创建一个MutationObserver对象监听一个DOM对象,同时在脚本中给它一次性添加10个子节点。回调函数进行输出标识处理,观察会有多少次输出。

  2. 针对第二个问题: 接着上面的测试方案,在回调函数中再给该DOM对象添加一个子节点。

  3. 针对第三个问题: 接着上面的测试方案,在脚本中先创建一个定时器宏任务,这个宏任务所在的任务(任务即指宏任务->微任务->渲染更新这个流程)一定在脚本(脚本可看做宏任务)所在的任务之后。在回调函数中设置执行次数限制,考虑到如果回调函数在本次执行,那么还会到下一步渲染更新,渲染更新后重新执行微任务队列回调函数,这种假设不加限制会形成无限循环。

<div id="div" style="height: 100px; width: 100px; background: blue"></div>
   <script>
       // 针对问题3:创建一个定时器宏任务
       setTimeout(() => {
           console.log("正在执行setTimeout的回调函数")
       }, 0)

       // 针对问题3:对修改DOM元素的次数进行限制
       let cnt = 0
       
       // 针对问题1:获取DOM元素
       let div = document.getElementById("div")
       
       // 针对问题1:注册一个监听对象,mutationRecords是存储监听到的修改操作的数组
       let observer = new MutationObserver((mutationRecords) => {
           // 针对问题1:在回调函数中标识输出
           console.log("开始执行MutationObserver的回调函数")

           // 针对问题3:对修改DOM元素的次数进行限制
           if(cnt >= 1)
               return

           // 针对问题2:在回调函数中修改监听的DOM元素
           let p = document.createElement("p")
           p.style.cssText = "height: 10px; width: 10px; background: red; margin: 0px; padding: 0px;"
           div.prepend(p)
           cnt ++
       })
       
       // 针对问题1:确定监听对象为div,监听所有属性和所有子节点
       observer.observe(div, {
           attributes: true,
           childList: true
       })
       
       // 针对问题1:对监听对象进行多次修改,观察是否会有10个回调函数进入微任务队列
       for(let i = 0; i < 10; i++) {
           let p = document.createElement("p")
           p.style.cssText = "height: 10px; width: 10px; background: red; margin: 0px; padding: 0px;"
           div.prepend(p)
       }
   </script>

测试结果

在这里插入图片描述

测试结论

(1) 多次修改DOM元素,只会向微任务队列添加一个回调函数。注:红宝书描述是,当触发监听元素的修改操作且仅当微任务队列为空时才会添加回调函数,从实际表现上来说跟这个总结意义相同

(2) 在回调函数中修改监听的DOM元素,那么会再次向微任务队列中添加一个回调函数

(3) 执行顺序是在回调函数中修改DOM后回调函数结束,本次微任务队列执行完毕。开始渲染更新,将修改内容更新,监听器感知到更新后向微任务队列中加入新的回调函数。渲染更新结束后询问微任务队列中是否还有未执行任务,发现了刚刚加入的回调函数,于是重新回到微任务队列执行阶段开始重复上述操作。

在这里插入图片描述

所以Event Loop中的一次任务执行可以用更详细的图来描述,这个图的流程也是可考证的,下面是WhatWG对渲染更新结束后的描述。

If all of the following are true:

  • this is a window event loop
  • there is no task in this event loop’s task queues whose document is fully active
  • this event loop’s microtask queue is empty
  • hasARenderingOpportunity is false

then for each Window object whose relevant agent’s event loop is this event loop, run the start an idle period algorithm, passing the Window. [REQUESTIDLECALLBACK]

2.queueMicrotask

介绍:

  1. queueMicrotask被挂载到了window对象下和者工作线程的全局对象下,参数是一个函数,可以把这个函数推入微任务队列

  2. 经查阅WhatWG规范和MDN,发现queueMicrotask是在2021年2月新的HTML标准中出现,而MDN中是2021年11月3日最后更新,目前除了IE浏览器,其它高版本的浏览器都支持queueMicrotask

简单测试:

  <script>
       Promise.resolve().then(() => {
           console.log("正在执行promise.then的回到函数")
       })
       queueMicrotask(() => {
           console.log("通过queueMicrotask把函数放入微任务队列")
       })
       Promise.resolve().then(() => {
           console.log("正在执行promise.then的回到函数")
       })
  </script>

测试结果:

在这里插入图片描述

3.setImmediate和setTimeout

问题:

(1) setTimeout(fn, 0) 和 setImmediate(fn)在node中的执行顺序并非是首先执行setTimeout的回调。可能是先执行setTimeout的回调,也可能先执行setImmediate的回调。

(2) 如果这两个函数放在IO操作的回调函数同时使用,那么一定是先执行setImmediate的回调

解释:

(1) setTimeout(fn, 0) 阈值设为0是不合法的,定时器会自动将其设为1。windows下时间分辨率为15ms。也就是说定时器容器–红黑树,遍历的周期是15ms。node进程分配的时间片可能导致事件循环第一次到达timers阶段没有检测到定时器已经到期,因为还不是红黑树遍历的时间。这就意味着当定时器阈值很小时,检测到setTimeout过期至少需要一轮事件循环。

(2) 因为poll阶段如果队列为空会先考虑setImmediate的情况,如果有setImmediate那么会进入check阶段。如果没有setImmediate,那么会查看是否有到期的定时器,如果有到期定时器,那么会回到timers阶段。

4.事件模型

  1. DOM0级事件模型:

DOM0级中的事件模型只能写事件处理程序来处理事件,没有事件监听器的概念,事件处理程序可以写在html标签中,也可以在脚本中通过设置对应元素的事件属性来处理事件。在DOM0中并没有定义事件流,事件不能冒泡,也不存在事件捕获。

注:事件处理程序始终在事件目标对象的作用域下执行,this=事件目标对象
  1. DOM2级事件模型:

DOM1级模型没有定义事件相关的内容。DOM2级模型定义了事件监听器。DOM2级模型定义了事件流的概念,事件总共有三个阶段,第一个是捕获阶段,第二个是处理阶段,第三个是冒泡阶段。

注:事件处理程序和事件监听器始终在事件目标对象的作用域下执行,this=事件目标对象
  1. IE浏览器中的事件模型:

IE9之前浏览器只支持事件处理和事件冒泡。IE9及之后的版本支持DOM2完整的事件流模型,可以使用addEventListener,可以在事件捕获阶段处理事件。

注:老版本的IE支持attachEvent和detachEvent,用法类似addEventListener和removeEventListener,只支持事件处理阶段和冒泡阶段。这里不做赘述。

5.事件触发

5.1事件触发过程

  • 浏览器进程下的UI线程感知到DOM事件,通知浏览器渲染进程下的主线程,document开始向事件触发处传播事件。此时是事件捕获阶段。

  • 事件传播到事件触发处,执行事件处理程序或者事件监听器注册的回调函数

  • 事件从传播处向document传播。此时是事件冒泡阶段。

5.2事件捕获

在addEventListener中第三个参数可以控制处理事件的函数是否在事件捕获阶段执行。

addEventListener("event", function(event){}, {
    capture: true, // 控制是否在事件捕获阶段执行
    once: true,    // 控制事件处理函数是否只执行一次
    passive: true  // 事件处理函数中是否不能使用event.preventDefault()阻止原生事件发生
})

5.3事件冒泡

在事件处理函数中可以手动设置来阻止事件冒泡。

addEventListener("event", function(event){
    event.preventDefault()   // 阻止原生事件的发生
    event.stopPropagation()  // 阻止事件冒泡
    event.stopImmediatePropagation()  // 阻止事件冒泡,阻止该触发事件对象上其它注册的事件处理函数执行
})

5.4不冒泡事件

scroll,blur(失去焦点),focus(获得焦点),mouseenter(鼠标移动到某个位置),mouseleave(鼠标离开某个位置),Media(图像,视频,音频)触发的事件,自定义事件默认不冒泡

6.事件委托

6.1 事件委托概念

事件委托指利用事件冒泡机制,使子元素上触发的事件被父元素监听。

6.2 事件委托和事件冒泡比较

<div style="background: green; width: 200px; height: 200px;" onclick="console.log(111, event.target);">
   <div style="width: 100px; height: 100px; background: blue;" onclick="console.log(111, event.target);">
      <div style="width: 50px; height: 50px; background: red;"></div>
   </div>
</div>

当点击红色方块时,先检查红色方块有无事件监听,执行完后再向上传播。事件委托就是利用这一点,不在红色方块上注册事件监听。

6.3 事件委托应用

  1. (监听大量子元素事件) 大量相同地位的子元素上会触发相同的事件,如果每个元素都注册一个事件处理函数,那么内存开销会陡然增加,此时适使用事件委托。

  2. (监听动态元素事件) 有些元素会被动态添加或删除,如果要监听这些元素上的事件,每次修改操作都重新注册或移除事件处理函数效率会低下,此时适合用事件委托。


主要参考:

[1] WhatWG的事件循环标准:https://html.spec.whatwg.org/multipage/webappapis.html#event-loops

[2] StackOverFlow宏任务和微任务区别:https://stackoverflow.com/questions/25915634/difference-between-microtask-and-macrotask-within-an-event-loop-context

[3] W3C的事件循环标准标准:https://www.w3.org/TR/2011/WD-html5-20110525/webappapis.html#definitions-1

[4] MDN规范:https://developer.mozilla.org/zh-CN/docs/Web

[5] <<JavaScript高级程序设计(第四版)>>

[6] StackOverFlow的setTimeout(fn,0)和setImmediate(fn)的区别:https://stackoverflow.com/questions/24117267/nodejs-settimeoutfn-0-vs-setimmediatefn

[7] NodeJS官方文档中文版:https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/

[8] JavaScript定时器性能优化:https://www.cnblogs.com/taocom/archive/2013/05/02/3054147.html

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vanghua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值