如何理解js的事件循环机制

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

浏览器

浏览器是提供了多个线程 浏览器同时也是多进程的,比如浏览器的每个tab标签页都是一个独立的渲染进程 在浏览的线程下又有: js引擎线程, HTTP请求线程, 定时触发线程, 时间触发线程, GUI线程,这些线程为js在浏览器中完成异步任务提供了技术基础

事件驱动

浏览器异步触发的原理实际上背后是一套时间驱动的机制 事件触发,任务选择,任务执行都是事件驱动来完成的,nodejs 和浏览器的设计都是基于事件驱动的 事件循环就是在事件驱动模式中来管理和执行事件的一套流程

Even Loop即事件循环

首先js的是单线程的,浏览器是多线程,执行js代码的线程只有一个是浏览器提供的js引擎线程 如何在执行的过程中不造阻塞,浏览器的node提供了事件循环机制来防止js单线程运行时 ,浏览器和node在执行js单线程时不会阻塞的一种机制,同时事件事件循环机制也就是我们经常使用异步的原理

浏览器中的事件循环

在js中,任务被分为两种,一种宏任务,一种微任务

宏任务和微任务

1. 宏任务:

  • setTimeout
  • setInterval
  • setImmediate (浏览器暂时不支持,只有IE10支持,具体可见
  • requestAnimationFrame (浏览器独有)
  • I/O
  • UI rendering (浏览器独有)

2. 微任务:

  • Promise async awit
  • Object.observe
  • MutationObserver

对比: 宏任务特征: 有明确的异步任务需要执行和回调,需要其他异步线程支持 微任务特征: 没有明确的异步任务需要执行,只有回调,不需要其他异步线程支持

3. 为什么区分宏任务和微任务

  • 调用栈

调用栈是一个后进先出的数据结构,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移除,直到栈内被清空

  • 任务队列

即队列,是一种先进先出的一种数据结构。

同步任务和异步任务

js单线程任务被分为同步任务和异步任务

同步任务会在调用栈中按照顺序等待主线程依次执行 异步任务会在异步任务有了结果之后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行

为什么区分?

扫描二维码关注公众号,回复: 13772930 查看本文章

image.png 异步 调用栈 消息队列 setTimeOut 微任务队列 执行完了之后就会evenloop循环

微任务比宏任务先执行 宏任务比微任务之间隔了一个DOM渲染

为什么要区分红任务和微任务 任务队列 先进先出 如果有优先级的任务引入微任务

node中的事件循环

将回调添加到轮询队列中以最终的执行

  • 循环 + 任务队列的流程
  • 微任务优先于宏任务

nodejs中其他常见的异步形式 :

  • 文件I/O读取-异步加载本地文件
  • setimmediate() -与settimeout设置0ms类似,在某些同步代码完成后立马执行
  • process.nextTick()- 在某些同步任务完成后立即执行
  • server close 等关闭回调

nodejs中的事件循环主要是在libuv库中执行的

nodejs的跨平台和事件循环机制都是预约libuv库的

libuv库怎么循环 :

  1. timers阶段,执行所有settimeout() setinterval()的回调
  2. pending callback 某些系统操作额的回调(比如tcp连接错误)
  3. idle prepare 仅是node内部使用
  4. poll 轮训等待新的链接和请求等事件,执行I/O回调等
  5. check setimmediate回调函数执行
  6. close callback 关闭回调执行,如socket.on('close',...)

实际上在node v10及以前的过程:

  1. 执行完一个阶段中的所有任务
  2. 执行nextTick队列里的内容
  3. 执行完微任务队的内容

但是在node v10以后和浏览器的行为一致了

timers

执行setTimeoutsetInterval中到期的callback,执行这两者需要设置一个毫秒数,理论上应该是时间一到就立即执行callback回调,但那是由于system的调度可能会延时,达不到预期的时间

官方文档的例子:

const fs = require('fs');

function someAsyncOperation(callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;

  console.log(`${delay}ms have passed since I was scheduled`);
}, 100);


// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
  const startCallback = Date.now();

  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    // do nothing
  }
});

复制代码

当进入时间循环时,他有一个空队列(fs.readFile()尚未完成),因此定时器将等待剩余毫秒数,当达到95ms时u,fs.readFile()完成读取文件并其完成需要10毫秒的回调被添加到轮询队列并执行

当回调结束时,队列中不再有回调,因此事件循环将看到已达到最快定时器的阈值,然后会回到timers阶段以执行定时器的回调

因此在此实例中,将看到正在调度的计时器与正在执行的回调之间的总延时将为105ms

poll

该poll阶段有两个主要功能

  • 执行I/O回调
  • 处理轮询队列中的事件(回到timer阶段执行回调)

当事件循环进入poll阶段并且在timer中没有可以执行定时器时,将发生以下两种情况之一:

  1. 如果poll队列不为空
  • 事件循环将遍历其同步执行他们的callback队列,直到队列为空,或者达到system-dependent(系统相关限制)

2.poll队列为空,则会发生以下两种情况

  • 如果setImmediate()回调需要执行,则会立即停止执行poll阶段并进入执行check阶段以执行回调
  • 如果没有setImmediate()回调需要执行,会等待回调被加入到队列中并立即执行回调,这里也有个超时的设置防止一直等待下去

设定了timer的话且poll队列为空,则会判断是否有timer超时,如果有的话回到timers阶段执行回调

check

此阶段允许人员在poll阶段完成后立即执行回调

setImmediate()的回调会被加入chenk队列中,从event loop 的阶段图可以知道,check阶段我的执行顺序在poll阶段后,如下例子

console.log('start')
setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(function() {
    console.log('promise1')
  })
}, 0)
setTimeout(() => {
  console.log('timer2')
  Promise.resolve().then(function() {
    console.log('promise2')
  })
}, 0)
Promise.resolve().then(function() {
  console.log('promise3')
})
console.log('end')


复制代码

start=>end=>promise3=>timer1=>timer2=>promise1=>promise2

思考

  1. 每一轮Eventloop都会伴随着渲染吗
  2. requestAninmationFrame在哪个阶段执行,在渲染之前还是渲染之后?在microTask前还是后
  3. requestIdleCallback 在哪个阶段执行?如何去执行?在渲染前还是后?在 microTask 的前还是后?
  4. resizescroll这些事件都是如何去派发的

总结事件循环

定义 事件循环是为了协调事件,用户交互,脚本,渲染,网络任务

  1. 从宏任务队列中取出一个宏任务并执行
  2. 检查微任务队列,执行并且清空微任务队列,如果在微任务队列中又加入了新的微任务,也会在这一步一起执行
  3. 进入更新渲染阶段,判断是否需要渲染,这里有rendering opportunity 的概念,也就是说不一定每一轮eventloop都会对应一次浏览器渲染,要根据屏幕刷新率,页面性能,页面是否在后台运行来共同决定,通常来说这个渲染间隔是固定的(所以多个task很可能再一次渲染之间执行)
    1. 浏览器会尽可能的保持帧率稳定,例如页面性能无法维持60fps(每16.66ms渲染一次)的话,那么浏览器就会选择30fps的个更新速率,而不是偶尔丢帧
    2. 如果浏览器上下文不可见,那么页面会降低到4fps左右甚至更低
    3. 如果满足以下条件也会跳过渲染
      • 浏览器判断更新渲染不会带来视觉上的改变
      • 帧动画回调为空,可以通过requestAninmationFrame来请求帧动画
  4. 如果上述判断决定本来不需要渲染,那么下面几部也不继续运行
  5. 对于需要渲染的文档,如果窗口的大小发生了变化,执行监听的resize方法
  6. 对于需要渲染的文档,如果页面发生了滚动,执行scroll方法
  7. 对于需要渲染的文档,执行帧动画回调,也就是requestAninmationFrame的回调
  8. 对月需要渲染的文档,执行IntersectionObserver (当其监听到目标元素的可见部分穿过了一个或多个阈(thresholds)时,会执行指定的回调函数。)
  9. 对于需要渲染的文档,重新渲染绘制用户界面
  10. 判断task队列和microTask队列是否都为空,如果是的话则进行Idle空闲周期的算法,判断是否要执行requestIdleCallback的回调函数

对于resize和scroll来说,并不是到了这一步采取执行滚动和缩放

猜你喜欢

转载自juejin.im/post/7083460951491657758