JS 事件循环机制

355 阅读6分钟

1 事件循环机制

截屏2021-04-13 13.19.29.png 因为js 是单线程的,如果有两个线程对DOM的操作有冲突的,为了解决阻塞的情况,所以有一个event loop 机制

当javascript代码执行的时候会将不同的变量存于内存中的不同位置:堆(heap)和栈(stack)。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针。

当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。 这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,变量以及这个作用域的this对象。

而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是一系列的方法被排队在执行栈中。

当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。

如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。

这个过程反复进行,直到执行栈中的代码全部执行完毕。

一个方法执行会向执行栈中加入这个方法的执行环境,在这个执行环境中还可以调用其他方法,甚至是自己,其结果不过是在执行栈中再添加一个执行环境。这个过程可以是无限进行下去的,除非发生了栈溢出。以上的过程说的都是同步代码的执行。

js引擎遇到一个异步事件后,js会将这个事件加入事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈执行完毕,它会立刻先处理微任务队列中的事件,然后再去宏任务队列中取出一个事件。

主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。

2 宏任务和微任务

不同的异步任务被分为两类:

  • 宏任务:整体代码,,setTimeout(), setInterval()

  • 微任务(micro task):new Promise().then里面的回调

  • 为什么要区分宏任务和微任务: 由于所有的任务保存在事件队列中,是先进先出的,但有些任务的优先级很高,所以引入了微任务的概念,为了保证任务完成的顺序。 js会先完成宏任务后,在完成微任务队列中的任务。

3 Node中的事件循环

宏任务执行顺序:

 1 timer 定时器: 执行已经安排的setTimeoutsetInterval 的回调函数
 2 pending callback 待定回调:执行延迟到下一个循环迭代的I/O 回调
 3 idle,prepare:仅系统内部使用
 4 Poll: 检索新的I/O事件, 执行I/O 回调
 5 check:执行setImmediate()回调函数
 6 close callbackssocket.on('close',()=>{})

微任务 和 宏任务 在 node 中的执行顺序

Node v10 及以前:

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

Node V10以后的:和浏览器的行为统一了

案例

//1 函数async1定义,但未调用,所以现在不输出
async function async1() {
  // 6
  console.log('async1 start')
  //7
  await async2()//比较重要的点,await 中执行的函数async2(),可以理解为将async2()放入 new Promise(() =>{})里,执行async2,打印async2
  //8 后面的的执行语句都相对于放在了 .then()中,属于微任务,加入了微任务队列中,先不执行,执行当前的宏任务)
  //13 第一个微任务,打印
  console.log('async1 end')
}
//2 函数async2定义,但未调用,所以现在不输出
async function async2() {
  console.log('async2')
}

//3 打印
console.log('script start')

// 4 setTimeout是宏任务,优先级低于微任务,会被移到下一次的宏任务队列中去,现在不输出
setTimeout(function () {
  //15 打印
  console.log('setTimeout')
},0)

// 5 这里执行async1,所以输出 'async1 start'
async1()

//9
new Promise(function (resolve) {
  // 10 同步代码,打印
  console.log('Promise1')
  //11 .then是异步的,是微任务,先不执行
  resolve()
}).then(function () {
  //14 第二个微任务,打印,此时,微任务队列清空了,接下来开启下一轮的宏任务,也就是setTimeout
  console.log('Promise2')
})
//12 打印,此时第一遍的宏任务已经执行完成,接下来清空微任务队列
console.log('script end')

/*打印顺序:
script start
async1 start
async2
Promise1
script end
async1 end
Promise2
setTimeout
*/
//1 打印
console.log('start')

// 2 宏任务,先不执行,放到下一轮
//8 执行第二行宏任务
setTimeout(()=>{
  // 9 打印
  console.log('children2')
  //10 这里相对于直接将.then中的回调加入了微任务队列中,此时,第二轮宏任务结束,接下来清空微任务队列,
  Promise.resolve().then(()=>{
    //11 打印
    console.log('children3')})
},0)

// 3
new Promise(function (resolve, reject) {
  // 4 同步的,打印
  console.log('children4')
  // 5 宏任务,先不执行,放到下一轮
  // 12 执行第三轮宏任务
  setTimeout(function () {
    //13 打印
    console.log('children5')
    // 16 resolve把Promise的状态改为fullfilled,//这里的children6会放入res中
    resolve('children6')
  },0)
  //6微任务,先不执行。
  // 7.then是需要Promise中有result,然后才可以添加到.then中,但setTimeout是一个宏任务,所以回调没有添加到微任务队列中去
  
}).then((res)=>{
  //14 打印
  console.log('children7')
  // 15 宏任务执行
  setTimeout(()=>{
    // 17 打印
    console.log(res)
  },0)
})
/*
* start
* children4
* 第一轮宏任务结束,接下来清空微任务队列,但没有微任务
* 尝试执行下一轮宏任务
* children2
* 第二轮宏任务结束,接下来清空微任务队列,
* children3
* 尝试执行第三轮宏任务
* children5
* children7
* children6
* */
const p = function () {
  //1 宏任务
  return new Promise((resolve, reject) => {
    // 2 宏任务
    const p1 = new Promise((resolve, reject) => {
      setTimeout(()=>{
        resolve(1)
      },0)
      resolve(2)
    })
    // 3 放入微任务队列
    p1.then((res)=>{
      console.log(res)//2
    })
    // 4 打印
    console.log(3)
    resolve(4)
  })
}
// 5 放入微任务队列
p().then((res)=>{
  console.log(res)//4
})
// 4 打印
console.log('end')

/*
*3
* end
* 2
* 4
* */