面试高频的事件循环

事件循环(Event Loop)

JavaScript是一门单线程的语言,在同一时间只能做一件事,js里面的代码要按照顺序逐行执行代码?比如说我们浏览新闻想要获取新闻图片,如果网络卡顿,获取了很长时间,难道就这样干等着吗?那肯定是不会的,给到用户的体验也是不好的

在js里所有的任务可以被分为同步任务异步任务,异步任务里又有微任务宏任务,

当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务

Snipaste_2023-08-09_21-11-15.png

事件循环运行机制

这种事件循环机制是由 JavaScript 的宿主环境来实现的,在浏览器运行环境中由浏览器内核引擎实现,而在 NodeJS 中则由 libuv 引擎实现。

主线程运行时候,产生堆(Heap)和栈(Stack),栈中的代码调用各种外部 API,它们在任务队列中加入各种事件。只要栈中的代码执行完毕,主线程就会通过事件循环机制读取任务队列,依次执行那些事件所对应的回调函数。

运行机制:

  1. 所有同步任务都在主线程上执行,形成一个 执行栈(Execution Context Stack)
  2. 主线程之外,还存在一个 任务队列(Task Queue)。只要异步任务有了运行结果,就在 任务队列 之中放置一个事件
  3. 一旦 执行栈 中的所有同步任务执行完毕,系统就会读取 任务队列,看看里面有哪些待执行事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行
  4. 主线程不断重复上面的第三步

浏览器环境

1633593121990_图片1.png

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

微任务:一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前

常见的微任务有:

*   Promise.then
*   MutaionObserver
*   Object.observe(已废弃;Proxy 对象替代)
*   process.nextTick(Node.js)

宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合

常见的宏任务有:

*   setTimeout/setInterval

*   UI rendering/UI事件

*   postMessage、MessageChannel

*   setImmediate、I/O(Node.js)

微任务与宏任务的区别

宏任务与微任务的区别在于队列中事件的执行优先级。进入整体代码(宏任务)后,开始首次事件循环,当执行上下文栈清空后,事件循环机制会优先检测微任务队列中的事件并推至主线程执行,当微任务队列清空后,才会去检测宏任务队列中的事件,再将事件推至主线程中执行,而当执行上下文栈再次清空后,事件循环机制又会检测微任务队列,如此反复循环。

宏任务与微任务的优先级

  • 宏任务的优先级高于微任务
  • 每个宏任务执行完毕后都必须将当前的微任务队列清空
  • 第一个 <script> 标签的代码是第一个宏任务
  • process.nextTick 优先级高于 Promise.then

Snipaste_2023-08-09_22-19-19.png
示例代码

1
2
3
4
5
6
7
8
9
10
11
12
setTimeout(function() {
console.log('1');
})

new Promise(function(resolve) {
console.log('2');
}).then(function() {
console.log('3');
})

console.log('4'); // 2 4 3 1

  • 这段代码作为宏任务,进入主线程。
  • 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)
  • 接下来遇到了Promisenew Promise立即执行,then函数分发到微任务Event Queue。
  • 遇到console.log(),立即执行。
  • 好啦,整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
  • ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event QueuesetTimeout对应的回调函数,立即执行。
  • 结束。

async与awite

async关键字是asynchronous(异步)的简写,用来声明一个函数是异步函数,写在函数的最前面,他会返回一个promise对象

awite可以理解为asynchronous waite(等待异步),他会等待一个异步任务返回的结果

下面这两种方法是等效的

1
2
3
4
5
6
7
function fn() {
return Promise.resolve('TEST');
}

async function asyncFn() {
return 'TEST';
}

awite

正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值

1
2
3
4
5
6
async function fn(){
// 等同于
// return 123
return await 123
}
fn().then(a => console.log(a)) // 123

不管await后面跟着的是什么,await都会阻塞后面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
async function fn1 (){
console.log(1)
await fn2()
console.log(2) // 阻塞
}

async function fn2 (){
console.log('fn2')
}

fn1()
console.log(3)
//1 fn2 3 2

上面的例子中,await 会阻塞下面的代码(即加入微任务队列),先执行 async 外面的同步代码,同步代码执行完,再回到 async 函数中,再执行之前阻塞的代码