Samgao0312 / Blog

MIT License
1 stars 1 forks source link

【再学前端】JS中的事件循环、宏任务与微任务 #116

Open Samgao0312 opened 2 years ago

Samgao0312 commented 2 years ago

“事件循环(Event Loop)机制” == “js 的执行机制问题”

首先,JavaScript是一个单线程的脚本语言。js代码会一行一行的执行,如果碰到定时器,网络请求等这些基于回调函数的任务,假如等待他执行完成以后再进行下一步操作的话,这是不合理的。所以我们有一个专门的 队列 来存放这些任务(如:DOM事件、定时器、网络请求),而这些任务又被称为异步任务

事件循环(Event Loop)简单来说就是在程序本身运行的主线程会形成一个"执行栈",除此之外,设立一个"任务队列",每当有异步任务完成之后,就会在"任务队列"中放置一个事件,当"执行栈"所有的任务都完成之后,会去"任务队列"中看有没有事件,有的话就放到"执行栈"中执行。

这个过程会不断重复,这种机制就被称为事件循环(Event Loop)机制。而宏任务和微任务靠这个机制来按顺序的执行。

1 事件循环(EventLoop)

Javascript 的事件分为同步任务异步任务(又分宏任务和微任务),遇到同步任务就放在执行栈中执行,而碰到异步任务就放到 任务队列之中,等到执行栈执行完毕之后,再去执行任务队列之中的事件。

执行栈 是一个存储函数调用的栈结构,遵循先进后出的顺序。

image

JS引擎常驻于内存中,等待宿主将JS代码或函数传递给它。 也就是等待宿主环境分配宏观任务,反复等待 - 执行即为事件循环。

Event Loop中,每一次循环称为tick,每一次 tick 的任务如下:

  1. 优先执行一个宏任务。执行栈选择最先进入队列的宏任务(一般都是script),执行其同步代码直至结束;
  2. 检查是否有微任务,有:执行所有微任务,执行至微任务队列为空;
  3. 如果宿主为浏览器,可能会渲染页面;
  4. 开始下一轮tick,开始下一个宏任务 —— 执行宏任务中的异步代码(setTimeout等回调)。

2 宏任务和微任务

定义:

  • 宏任务: 常见的定时器,用户交互事件等等.(宏任务就是特定的这些个任务,没什么特殊含义)
  • 微任务: Promise相关任务,MutationObserver等(一样,只是一种称呼而已!!!)

ES6 规范中,microtask 称为 jobs,macrotask 称为 task。 宏任务是由宿主发起的,而微任务由JavaScript自身发起。

image

image

3 宏任务有哪些?微任务有哪些?

宏任务可以被理解为每次"执行栈"中所执行的代码,而浏览器会在每次宏任务执行结束后,在下一个宏任务执行开始前,对页面进行渲染。宏任务包括:

script(整体代码)
setTimeout
setInterval
I/O
UI交互事件
Dom渲染
postMessage
MessageChannel
setImmediate(Node.js 环境)
requestAnimationFrame

微任务,可以理解是在当前"执行栈"中的任务执行结束后立即执行的任务。而且早于页面渲染和取任务队列中的任务。微任务包括:

Promise.then
await
Object.observe
MutationObserver
process.nextTick(Node.js 环境)

4、为什么微任务先于宏任务执行?

因为当主线程(执行栈)的代码执行完毕之后,在Event Loop执行之前,首先会尝试DOM渲染,这个时候,微任务是在DOM渲染之前执行,DOM渲染完成了之后会执行宏任务,所以,微任务要比宏任务更早执行!!! 那么我们的最终图示如下

image

JS 代码的执行顺序和规则(Event Loop):

  1. 同步代码,一行一行放在Call Stack执行;
  2. 遇到异步,会先‘记录’下来,等待时机(如定时器,网络请求等等);
  3. 时机到了,就会移步到Call Queue;
  4. 如过call Stack为空,就是同步代码执行完毕之后,会尝试DOM渲染(微任务在DOM渲染前执行,宏任务在DOM渲染后执行);
  5. Event Loop 开始工作,轮询查找Callback Queue,如果有代码就移动到Call Stack中;
  6. 不停的轮询查找;

5、 思考题

题-1:

console.log('start')

setTimeout(() => {
  console.log('setTimeout')
}, 0)

new Promise((resolve) => {
  console.log('promise')
  resolve()
})
  .then(() => {
    console.log('then1')
  })
  .then(() => {
    console.log('then2')
  })

console.log('end')

上面代码一次输出:

start 
promise
end
then1
then2
setTimeout

题-2:

function app() {
  setTimeout(() => {
    console.log("1-1");
    Promise.resolve().then(() => {
      console.log("2-1");
    });
  });
  console.log("1-2");
  Promise.resolve().then(() => {
    console.log("1-3");
    setTimeout(() => {
      console.log("3-1");
    });
  });
}
app();

// 依次输出
1-2
1-3
1-1
2-1
3-1

题-3:

const promise = new Promise((resolve, reject) => {
    console.log(1);
    resolve();
    console.log(2);
})

promise.then(() => {
    console.log(3);
})

console.log(4);
// 依次输出: 1   2   4   3

解析:promise是建立后立即执行,所以会先输出 1,2,而 Promise.then() 内部的代码在 当次 事件循环的 结尾 立刻执行 ,所以会继续输出4,最后才是输出3。

6、参考阅读