JS进阶 | 浅谈JavaScript中的进程、宏任务、微任务

404 阅读4分钟

JS进阶系列文章

什么是进程和线程

  • 进程(process)计算机已经运行的程序,是操作系统管理程序的一种方式
  • 线程(thread)操作系统能够运行运算调度的最小单位,通常情况下它包含在进程中

JavaScript是单线程的,但JavaScript的线程有自己的容器进程 浏览器 / node

  • 浏览器是多线程的,每个tab会开启一个新的进程,每个进程里又有很多个线程,这包括执行JavaScript代码
  • JavaScript是在一个线程中执行的,只能同时处理一件事情
    • 如果这个事情是非常耗时的,当前进程就会被阻塞
  • 浏览器每个进程是多线程的,其他线程可以完成这个耗时的操作

在浏览器中,异步函数都会被js引擎放进一个事件队列中,当同步代码执行完毕之后js引擎会将这个队列中的函数一一取出执行。

下面这段代码,setTimeout会在1秒后,将这个异步函数放到浏览器的一个事件队列。当同步代码执行完毕后,在执行setTimeout中的异步函数。

注意,同步函数不会被放进事件队列中,直接能够被执行的代码一般被称之为 main script

console.log('script start');
// 业务代码
setTimeout(()=>{
  console.log('_island');
},1000)
console.log('script end');

宏任务微任务

宏任务(macro task)事件包括有:setTimeoutsetIntervalI/OpostMessageMessageChanelsetImmediateUI rendering

微任务(micro task)事件包括有:Promise.then、MutationnObserveObject.observeprocess.nextTick

规范:JavaScript引擎在执行任何的宏任务之前,都需要先保证微任务队列已经被清空

执行机制

  • 优先级
    • main script代码先执行
    • 在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微队列中是否有任务需要要执行,如果有,那么执行微任务队列中的微任务(回调)

根据上面的介绍,我们可以得出一张事件循环图

image-20220223114721605

下面,我们来看两道题,看看它们的输出顺序是怎么样的

console.log("script start");

function foo() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("setTimeOut1");
    });
    resolve(0);
  }).then((res) => {
    console.log(res);
  });
}

foo();

setTimeout(() => {
  console.log("setTimeOut2");
});

console.log("script end");

解析上面这一段代码的执行顺序

  • 首先会执行这段代码中的同步代码,先打印出script start

  • 执行foo函数,返回的是Promise对象,先将它放到微任务对象中

  • 打印输出script end

  • 执行微任务中的Promise对象,在这里将setTimeout函数存放到宏任务中,res(0)之后调用then方法,打印出0,执行完毕

  • 再次遇到setTimeout函数,同样存放到宏任务中。

  • 此时,微任务已经执行完毕了,接下来执行宏任务队列中的任务

  • 根据存放的顺序,打印出setTimeOut1setTimeOut2

  • 最终打印出来的是

    script start
    script end 
    0
    setTimeOut1
    setTimeOut2
    

在看另外一道题,多出asyncawait函数时,它的执行顺序会是怎么样的?

console.log("script start");

async function foo() {
  await new Promise((resolve) => {
    console.log("Promise1");
    resolve('Promise1 then')
  }).then((res)=>{
    console.log(res);
  })
}

async function bar() {
  console.log('bar1');
  await console.log(000);
  console.log('bar2')
}

foo();
bar()

console.log("script end");

解析上面这一段代码的执行顺序

在解析这道题之前,我们需要搞懂async/await函数的作用

  • 首先会执行这段代码中的同步代码,先打印出script start

  • 执行foo函数,执行Promise中的函数体,打印出Promise1,将then中的事件放到微任务队列中

  • 执行bar函数,打印出bar1000,接下来会将bar2放到微任务队列中(在await等价于then,后续的任务都会被放到微任务中)

  • 打印script end,调用微任务对象中的任务,打印Promise1 thenbar2

  • 最终打印出来的是

    script start
    Promise1
    bar1
    000
    script end
    Promise1 then
    bar2
    

总结

JavaScript是单线程的,采用单线程的事件循环方式管理异步任务,通过任务队列来处理异步任务。为了用户的体验微任务会被优先执行。