面试必考点——V8中的事件循环机制

512 阅读4分钟

前言

什么是事件循环机制?

由于JavaScript是单线程的,一次只能执行一个任务,事件循环机制能够让js在不阻塞的情况下处理异步操作。

  • 同步代码:不耗时执行的代码。
  • 异步代码:需要耗时执行的代码。
    • 微任务:promise.then(), process.nextTick(), MutationOverserver()
    • 宏任务:script, setTimeout, setInterval, setImmediate, I/O, UI-rendering(页面渲染)

执行步骤

  • 执行同步代码(这属于宏任务)
  • 同步代码执行完毕后,检查是否有异步代码执行
  • 若有,执行所有微任务
  • 微任务执行完毕后,如有需要就渲染页面
  • 执行异步宏任务,既是这次事件循环的结束也是开启下一次事件循环

什么是进程和线程?

  • 进程:CPU运行指令和保存上下文所需的时间
  • 线程:执行一段指令需要的时间

例如

一个浏览器的tab页面是一个进程,这个进程当中有多个线程。

  • 渲染线程
  • js引擎线程
  • http线程

注: js的加载会阻塞页面的渲染,渲染线程和js引擎线程不能同时工作。浏览器会把js代码执行完毕再执行html代码。

js单线程:v8在执行js的过程中,只有一个线程会工作

  1. 节约性能
let a = 1
console.log(a);

setTimeout(function () {
    let b = 2
    console.log(b);
    a++
}, 1000)

console.log(a);

function bar() {
    console.log('bar');
}
bar()

打印结果

image.png

分析

代码从上往下执行,由于setTimeout()定时器是耗时代码,v8执行的过程会跳过该段代码,继续执行下面的代码,直到把不耗时的代码执行完成后再来执行耗时代码。所以会先打印 1 1 bar,然后过1s再打印b的值2。

  1. 节约上下文切换的时间
let a = 1

for(var i = 0; i < 1000; i++) { // 不耗时
    a++
}
console.log(a);

setTimeout(function() {
    a++
    console.log(a);
}, 100)

打印结果

image.png

分析

由于js是单线程的并且for循环是不耗时代码,所以会先执行for循环语句,再执行定时器。

对于v8的事件循环机制的步骤

console.log(1);
new Promise((resolve, reject) => {
  console.log(2);
  resolve()
})
.then(() => {
  console.log(3);
  setTimeout(() => {
    console.log(4);
  }, 0)
})
setTimeout(() => {
  console.log(5);
  setTimeout(() => {
    console.log(6);
  }, 0)
}, 0)
console.log(7);

打印结果

image.png

分析

  • 代码从上往下执行,先执行同步代码console.log(1);,再执行Promise实例对象中的代码console.log(2);并且调用resolve()保证.then()能够触发,由于promise.then()是微任务,所以会存入到微任务队列中等待,setTimeout()(该定时器记为set1)是宏任务,会存到宏任务队列当中等待,再执行console.log(7);
  • 同步代码执行完成后,执行微任务promise.then(),所以就会执行console.log(3);并且把.then()内部的setTimeout()(该定时器记为set2)存到宏任务队列当中等待。
  • 微任务执行完成后,此时并没有页面需要渲染,所以执行宏任务set1,执行console.log(5);,将set1内部的定时器(记为set3)存放到宏任务队列中等待,set1执行完毕。再执行set2,执行console.log(4);,最后执行set3,执行console.log(6);,所以打印的结果依次是:1 2 7 3 5 4 6。

image.png

async关键字

在函数前加上async关键字,默认让该函数返回一个Promise实例对象。

let data = null
function getData() {
    setTimeout(() => {
      data = [1, 2, 3]
    }, 1000)
}
function another() {
  console.log(data);
}
getData()
another()

打印结果

image.png

分析

虽然getData()先被调用,但是该数内部含有一个定时器,为耗时代码,所以会将该定时器放入宏任务队列中等待,继续执行下面的代码,调用another(),打印data,此时的data内的值并未改变,所以打印结果是null。

于是引入Promise实例对象

let data = null
function getData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      data = [1, 2, 3]
      resolve()
    }, 1000)
  })
}
function another() {
  console.log(data);
}
getData().then(() => {
  another()
})

打印结果

image.png

分析

因为定时器被执行后才会调用resolve(),才能执行.then()方法,在定时器内部已经执行了data = [1, 2, 3],于是再调用another()函数时打印结果就为 [1, 2, 3]

引入async 和 await

let data = null
function getData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      data = [1, 2, 3]
      resolve()
    }, 1000)
  })
}
function another() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      data.push(4)
      resolve()
    }, 100)
  })
  
}
function another2() {
  console.log(data); // 1234
}

async function foo() {
    // return new Promise((resolve, reject) => {})
  await getData()
  await another()
  another2()
}
foo()

打印结果

image.png

分析 await getData()在getData()前面加上await,说明要等getData()执行完毕后才能继续执行下面的代码,getData()执行完毕后,data的值为[1, 2, 3],然后执行await another(),another()执行往data数组中插入一个数4,data的值为[ 1, 2, 3, 4 ],然后再调用another函数,打印出data的结果。 示例

console.log('script start');

async function async1() {
  await async2() // await 会将后续的代码阻塞进微任务队列
  console.log('async1 end');
}
async function async2() {
  console.log('async2 end');
}
async1()
setTimeout(function() {
  console.log('setTimeout');
}, 0)
new Promise(function(resolve, reject) {
  console.log('promise');
  resolve()
})
.then(() => {
  console.log('then1');
})
.then(() => {
  console.log('then2');
})
console.log('script end');

打印结果

image.png

分析 根据v8的事件循环机制,打印结果应该依次是

image.png

为什么与node打印结果不匹配呢?那是因为await 会将后续代码阻塞进微任务队列

image.png

总结

自己手敲一敲,试着理清楚循环机制的思路吧。

Thanks.jpg