# 事件循环机制

# 浏览器

# 阐述

JS 脚本 scrpit 包含异步任务和同步任务,首先同步任务在主线程上执行,当同步任务执行完成后,JS 将当前执行栈清空。然后开始执行异步任务,异步任务分为宏任务和微任务,异步任务会放入任务队列当中,宏任务放入宏任务队列,微任务会放入微任务队列。

同步任务执行完之后,先去微任务队列拿出一个微任务,如果执行过程产生了微任务或者宏任务,会将他们放入各自的任务队列。循环往复,直到把微任务队列清空。

紧接着会从宏任务队列中拿出一个宏任务,执行过程中若产生了微任务,首先会把微任务放入微任务队列,然后等这个宏任务执行完成之后就会立即去执行这个微任务,如果没有产生微任务就继续执行下一个宏任务,循环往复,直到清空宏任务。

在宏任务执行完成前的一刻,渲染进程会触发 GUI 和 dom 的渲染。

# 宏任务和微任务

微任务包括:process.nextTickpromiseMutationObserver

宏任务包括:scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

# 例题分析

console.log("script start");

setTimeout(function() {
  console.log("setTimeout");
}, 0);

Promise.resolve()
  .then(function() {
    console.log("promise1");
  })
  .then(function() {
    console.log("promise2");
  });
console.log("script end");
查看答案

script start script end promise1 promise2 setTimeout

执行栈: script start script end
微任务队列: promise1 promise2
宏任务队列: setTimeout

console.log("script start");

async function async1() {
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2 end");
}
async1();

setTimeout(function() {
  console.log("setTimeout");
}, 0);

new Promise(resolve => {
  console.log("Promise");
  resolve();
})
  .then(function() {
    console.log("promise1");
  })
  .then(function() {
    console.log("promise2");
  });

console.log("script end");
查看答案

script start async2 end async1 end promise1 promise2 setTimeout


分析: asyncawait 代码转换为

async function async1() {
  await async2()
  console.log('async1 end')
}
===function f() {
  return RESOLVE(p).then(() => {
    console.log('async1 end')
  })
}

首先执行同步任务

- script start
- async2 end
- Promise (promise的构造函数将其视为同步函数)
- script end
然后将webapi将异步任务队列分别丢进微任务队列和宏任务队列

首先执行微任务,执行微任务队列前会去检查当前任务队列是否存在在微任务队列或执行栈任务,
有就放入各自队列,直到清空微任务

- async1 end
- promise1
- promise2
- setTimeout
然后执行宏任务,执行宏任务前会去检查当前宏任务的微任务队列,若存在则一次性执行完,
不存在则执行宏任务。宏任务执行完成,渲染进程触发GUI,dom渲染。
依次循环

注意这里,如果浏览器的版本不一致,可能也导致不一样的结果

关于 73 以下版本和 73 版本的区别

  • 在老版本版本以下,先执行 promise1promise2,再执行 async1
  • 在 73 版本,先执行 async1 再执行 promise1promise2

# NodeJS

# 阐述

Node 的事件循环会经历 6 个阶段

  1. timers: 执行 setTimeout 和 setInterval 到了时间的回调

  2. pending back: 上一轮循环少数的 callback 会放在这一阶段执行

  3. idle,prepare: 内部使用

  4. poll: 最重要的阶段,中文叫做轮询阶段

完整解释

像诸如 文件 I/O 网络 I/O 等等这些一步操作执行完,会通知 JS 主线程。如何通知呢? 它会通过 dataconnect等事件使得事件循环到达轮询阶段,到了这个阶段之后如果:

  • 如果定时器到期了,会首先回到 timers阶段
  • 如果没有定时器,看回调函数队列 - 队列不为空: 依次执行队列的回调 - 队列为空:检查是否有 setImmdiate回调 - 有:进入check阶段 - 没有:继续等待,阻塞一段时间然后自动进入 check阶段
  1. check: 直接执行 setImmdiate 阶段
  2. close callback: 执行 close 事件的 callback,例如socket.on('close',fn)

# setTimeout、setImmediate() 和 process.nextTick()

  • setTimeout:为在指定时间之后就执行

  • setImmediate:设计是当前轮询阶段完成后就立即执行,如果放在同一个 I/O 循环内使用,setImmediate 总是被优先调用,他的优势是如果在 I/O 周期内被调度的话,他会在所有定时器之前执行。他执行的方式是当前任务队列的尾部

  • process.nextTick:触发的是在当前执行栈的尾部,下一次事件循环之前,总是在异步之前

为什么要使用 process.nextTick(),因为可以允许用户处理错误,或者是在事件循环继续之前重试请求

总结规律

在 文件I/O 、 网络I/O中,setImmediate会 先于 settimeout
否则一般情况下 setTimeout 会先于 setImmediate

# Node 版本对事件循环的影响

  • node 版本 >= 11 的(我们现在用的),它会和浏览器表现一致,一个定时器运行完立即运行相应的微任务

  • node 版本 < 11 ,当一个定时器运行完之后,会将过程中产生的微任务暂存,然后直接执行新的定时器任务,最后逐一执行完微任务

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

node <11: timer1,timer2,promise1,promise2
node >=11: timer1,promise1,timer2,promise2

# 例题分析

以下试题都是以 Node 环境执行结果

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");
查看答案

node >= 11: start,end,timer1,promise1,timer2,promise2

node < 11: start,end,timer1,timer2,promise1,promise2

首先执行主线程执行栈任务:打印start,end

然后执行微任务Promise,这一步和浏览器一致,打印promise3

然后进入timers阶段,这里会执行定时器到期的回调

如果node版本>=11, 则代表执行完一个定时器立即执行callback里面的微任务,打印timer1,promise1,timer2,promise2

如果node版本<11,则代表执行完定时器暂存微任务,继续执行定时器任务,最后执行微任务,打印timer1,timer2,promise1,promise2

setTimeout(() => {
  console.log("timeout");
}, 0);

setImmediate(() => {
  console.log("immediate");
});

const fs = require("fs");

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log("timeout");
  }, 0);
  setImmediate(() => {
    console.log("immediate");
  });
});
查看答案

timeout,immediate,immediate,timeout

首先需要掌握:immediate的机制
1.IO操作时,immediate其实就是轮询完成之后立即执行的函数,
如果有IO操作调度的话,他会优先于定时器函数,它的执行是发生在任务队列的尾部
2.IO操作,immediate会比定时器晚执行

所以依此规则:读取IO发生在 `timers` 阶段之后,打印timeout,immediate,immediate,timeout


超经典试题

let bar;
console.log("start");
const fs = require("fs");

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log("timeout");
  }, 0);
  setImmediate(() => {
    console.log("immediate");
  });
});
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");

function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log("bar", bar); // 1
});

bar = 1;
查看答案

node >=11: start,end,bar:1,promise3,timer1,promise1,timer2,promise2,immediate,timeout

node <11: start,end,bar:1,promise3,timer1,timer2,promise1,promise2,immediate,timeout

首先执行主线程的执行栈任务,打印start,end

process.nextTick的机制是发生在执行栈尾部,优先微任务,所以打印 bar:1

执行任务队列的微任务队列,打印promise3

IO操作发生在poll轮询阶段,所以先看timers阶段的定时器,
由于node版本问题会造成不同的答案,所以会有两种答案

node >= 11 打印timer1,promise1,timer2,promise2
node <11 打印timer1,timer2,promise1,promise2

接着到达poll阶段,由于有文件IO操作,
此时的seImmediate也就是check阶段调用的函数会优先于定时器先执行,所以打印 immediate timeout

# 浏览器和 Node 事件循环的区别

浏览器环境下微任务的执行是每个宏任务执行之后,而 node 中微任务会在各个阶段之间执行, 一个阶段结束立刻执行 mircroTask

# 异步输出结果

  • await 后面接一个会 return new promise 的函数并执行它
async function async1() {
  console.log('async1 start')
  await async2() // 会返回一个promise
  console.log('async1 end')
}
async function async2() {
  console.log('async2')
}
console.log('script start')
setTimeout(function () {
  console.log('setTimeout')
}, 0)
async1()
new Promise(function (resolve) {
  console.log('promise1')
  resolve()
}).then(function () {
  console.log('promise2')
})
console.log('script end')
输出:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
  • process.nextTick()要优于 promise.then 执行

node 中存在着一个特殊的队列,即 nextTick queue。这个队列中的回调执行虽然没有被表示为一个阶段,当时这些事件却会在每一个阶段执行完毕准备进入下一个阶段时优先执行。当事件循环准备进入下一个阶段之前,会先检查 nextTick queue 中是否有任务,如果有,那么会先清空这个队列。

process.nextTick(function() {
  console.log(7);
});

new Promise(function(resolve) {
  console.log(3);
  resolve();
  console.log(4);
}).then(function() {
  console.log(5);
});

process.nextTick(function() {
  console.log(8);
});
3;
4;
7;
8;
5;
  • 以下两题来源于面试
async function f() {
  await new Promise(resolve => {
    setTimeout(() => {
      console.log("1");
    }, 2000);
  });
  await new Promise(resolve => {
    setTimeout(() => {
      console.log("2");
    }, 3000);
  });

  console.log("3");
}

f();
// 2秒后输出1,原因是await之后返回的promise状态没有发生变化,一直是pending
// 在promise中加上   resolve() 就能打印  3 了
async function f2() {
  let promiseA = new Promise(resolve => {
    setTimeout(() => {
      console.log("1");
    }, 2000);
  });

  let promiseB = new Promise(resolve => {
    setTimeout(() => {
      console.log("2");
    }, 3000);
  });

  await promiseA;
  await promiseB;
  console.log("3");
}
f2();
// 2秒打印1,再过1秒打印2
// 这里3不打印的原因也是因为await返回的promise状态没有变化,一直是pending
// 在promise中加上   resolve() 就能打印  3 了
Last update: 7/20/2021, 8:53:54 AM