Skip to main content

浏览器-宏任务与微任务

进程和线程

  • 线程和进程是操作系统中的两个概念:
    • 进程( process) :计算机 已经运⾏的程序 ,是 操作系统管理程序 的⼀种⽅式;
    • 线程( thread) :操作系统能够运⾏ 运算调度的最⼩单位 ,通常情况下 它被包含在进程 中;
  • 听起来很抽象,这⾥还是给出我的解释:
    • 进程: 我们可以认为,启动 ⼀个应⽤程序 ,就会默认 启动⼀个进程 (也可能是多个进程);
    • 线程: 每 ⼀个进程 中,都会启动 ⾄少⼀个线程 ⽤来执⾏程序中的代码,这个线程被称之为 主线程 ; 所以我们也可以说进程是线程的容器;
  • 再⽤⼀个形象的例⼦解释:
    • 操作系统类似于⼀个⼤⼯⼚;
    • ⼯⼚中⾥有很多⻋间,这个⻋间就是进程;
    • 每个⻋间可能有⼀个以上的⼯⼈在⼯⼚,这个⼯⼈就是线程;

操作系统 – 进程 – 线程

截屏2023-07-14 16.54.43

操作系统的⼯作⽅式

  • 操作系统是如何做到同时让多个进程(边听歌、边写代码、边查阅资料)同时⼯作 呢?

    • 这是因为 CPU的运算速度⾮常快 ,它可以 快速的在多个进程之间迅速的切换 ;
    • 当我们 进程中的线程 获取到 时间⽚ 时,就可以 快速执⾏我们编写的代码 ;
    • 对于⽤户来说 是感受不到这种快速的切换 的;
  • 你可以在 Mac的活动监视器或者 Windows的资源管理器中查看到很多进程:

浏览器中的 JavaScript线程

  • 我们经常会说 JavaScript是单线程(可以开启 workers) 的,但是 JavaScript的线程应该有⾃⼰的容器进程 : 浏览器或者 Node。
  • 浏览器是⼀个进程吗,它⾥⾯只有⼀个线程吗?
    • ⽬前 多数的浏览器其实都是多进程 的,当我们 打开⼀个 tab⻚⾯时就会开启⼀个新的进程 ,这是为了 防⽌⼀个⻚⾯卡死⽽造成所有⻚⾯⽆法响应 ,整个浏览器需要强制退出;
    • 每个进程中⼜有很多的线程 ,其中 包括执⾏ JavaScript代码的线程 ;
  • JavaScript的代码执⾏是在⼀个单独的线程中执⾏的:
    • 这就意味着 JavaScript的代码,在 同⼀个时刻只能做⼀件事 ;
    • 如果 这件事是⾮常耗时 的,就意味着当前的线程就 会被阻塞 ;
  • 所以真正耗时的操作,实际上并不是由 JavaScript线程在执⾏的:
    • 浏览器的每个进程是多线程的,那么 其他线程可以来完成这个耗时的操作 ;
    • ⽐如 ⽹络请求、定时器 ,我们只需要在特性的时候执⾏应该有的回调即可;

浏览器的事件循环

  • 如果在执⾏ JavaScript代码的过程中,有异步操作呢?
    • 中间我们插⼊了⼀个 setTimeout的函数调⽤;
    • 这个函数被放到⼊调⽤栈中,执⾏会⽴即结束,并不会阻塞后续代码的执⾏;

function foo() {
console.log("foo function")
// 1.在JavaScript内部执行
// let total = 0
// for (let i = 0; i < 1000000; i++) {
// total += i
// }

// 2.创建一个定时器
setTimeout(() => {
console.log("setTimeout")
}, 10000);

console.log("bar function")
}

foo()

截屏2023-07-14 17.01.51

宏任务和微任务

  • 但是事件循环中并⾮只维护着⼀个队列,事实上是有两个队列:

    • 宏任务队列( macrotask queue) : ajax、 setTimeout、 setInterval、 DOM监听、 UI Rendering等
    • 微任务队列( microtask queue) : Promise的 then回调、 Mutation Observer API、 queueMicrotask()等
  • 早期浏览器其实只有宏任务队列,并没有微任务队列,加入微任务队列是为了让某些代码更快执行

  • 那么事件循环对于两个队列的优先级是怎么样的呢?

    • 1.main script中的代码优先执⾏ (编写的顶层 script代码);
    • 2.在 执⾏任何⼀个宏任务之前(不是队列,是⼀个宏任务) ,都会先查看微任务队列中是否有任务需要执⾏
      • 也就是宏任务执⾏之前,必须保证微任务队列是空的;
      • 如果不为空,那么就优先执⾏微任务队列中的任务(回调);
  • 有一个方法,可以将任务加入微任务中去

    • queueMicrotask()
      • queueMicrotask(() => {/* ... */});
      • 将微任务加入队列

宏任务和微任务执行案例

  • 全局代码执行,两个setTimeout的回调会被添加到宏任务的队列
  • 打印111111
  • promise类的回调参数会立即执行,打印222222,-------1,--------2
  • then方法的回调会被加入到微任务的队列中
  • 打印333333
  • 全局代码执行完,开始去执行宏任务里的任务,但在执行之前,会检查微任务队列中是否有待执行的任务
  • 微任务执行,打印then传入的回调: res
  • 微任务执行完毕,开始执行宏任务的任务,打印setTimeout0
  • 继续执行下一个宏任务的任务,检查微任务队列,没有任务。则执行宏任务的任务
  • 打印setTimeout1
// 定时器会被加入到宏队列
setTimeout(() => {
console.log("setTimeout0")
}, 0)
setTimeout(() => {
console.log("setTimeout1")
}, 0)


console.log("1111111")
new Promise((resolve, reject) => {
//promise的回调参数会立即执行
console.log("2222222")
console.log("-------1")
resolve()
console.log("-------2")
}).then(res => {
// Promise中的then的回调会被添加到微队列中
console.log("then传入的回调: res", res)
})
console.log("3333333")
//1111111
//2222222
//-------1
//-------2
//3333333
//then传入的回调: res undefined
//setTimeout0
//setTimeout1

async await的执行

  • async函数返回的是一个promise对象,所以会加入到微任务队列中
  • await跟的表达式也是一个promise对象,所以在await后面的代码,会等到这个promise对象确定状态之后再执行
    • 其实是await后面的代码加入了微任务队列
function requestData(url) {
console.log("requestData")
return new Promise((resolve) => {
setTimeout(() => {
console.log("setTimeout")
resolve(url)
}, 2000);
})
}

// 2.await/async
async function getData() {
console.log("getData start")
const res = await requestData("why")
//这里不会立即执行,等到requestData有了结果后再执行。其实就是加入到了微任务队列中
console.log("then1-res:", res)
console.log("getData end")
}

getData()
// getData start
// requestData
// script end
// setTimeout
// then1-res: why

宏任务和微任务面试题

console.log("script start")

setTimeout(function () {
console.log("setTimeout1");
new Promise(function (resolve) {
resolve();
}).then(function () {
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log("then4");
});
console.log("then2");
});
});

new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("then1");
});

setTimeout(function () {
console.log("setTimeout2");
});

console.log(2);

queueMicrotask(() => {
console.log("queueMicrotask1")
});

new Promise(function (resolve) {
resolve();
}).then(function () {
console.log("then3");
});

console.log("script end")


//script start
//promise1
//2
//script end
//then1
//queueMicrotask1
//then3
//undefined
//etTimeout1
//then2
//then4
//setTimeout2

async function async1 () {
console.log('async1 start')
await async2();
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

Node的事件循环

  • 浏览器中的 EventLoop是根据 HTML5定义的规范来实现的,不同的浏览器可能会有不同的实现,⽽ Node中是由 libuv实现的。

  • 这⾥我们来给出⼀个 Node的架构图:

    • 我们会发现 libuv中主要维护了⼀个 EventLoop和 worker threads(线程池);
    • EventLoop负责调⽤系统的⼀些其他操作:⽂件的 IO、 Network、 child-processes等
  • libuv是⼀个多平台的专注于异步 IO的库,它最初是为 Node开发的,但是现在也被使⽤到 Luvit、 Julia、 pyuv等其他地⽅;

Node事件循环的阶段

  • 我们最前面就强调过,事件循环像是一个桥梁,是连接着应用程序的JavaScript和系统调用之间的通道:
    • 无论是我们的文件IO、数据库、网络IO、定时器、子进程,在完成对应的操作后,都会将对应的结果和回调函数放到事件循环 (任务队列)中;
    • 事件循环会不断的从任务队列中取出对应的事件(回调函数)来执行;
  • 但是⼀次完整的事件循环 Tick分成很多个阶段:

    • 定时器( Timers) :本阶段执⾏已经被 setTimeout() 和 setInterval() 的调度回调函数。
    • 待定回调( Pending Callback) :对某些系统操作(如 TCP错误类型)执⾏回调,⽐如 TCP连接时接收到 ECONNREFUSED。
    • idle, prepare:仅系统内部使⽤。
    • 轮询( Poll) :检索新的 I/O 事件;执⾏与 I/O 相关的回调;
    • 检测( check) : setImmediate() 回调函数在这⾥执⾏。
    • 关闭的回调函数 :⼀些关闭的回调函数,如: socket.on('close', ...)。
  • Node事件循环的阶段图解

Node的宏任务和微任务

  • 我们会发现从⼀次事件循环的 Tick来说, Node的事件循环更复杂,它也分为微任务和宏任务:

    • 宏任务( macrotask): setTimeout、 setInterval、 IO事件、 setImmediate、 close事件;
    • 微任务( microtask): Promise的 then回调、 process.nextTick、 queueMicrotask;
  • 但是, Node中的事件循环不只是 微任务队列和 宏任务队列:

    • 微任务队列:

      • next tick queue: process.nextTick;
      • other queue: Promise的 then回调、 queueMicrotask;
    • 宏任务队列:

      • timer queue: setTimeout、 setInterval;
      • poll queue: IO事件;
      • check queue: setImmediate;
      • close queue: close事件;

Node事件循环的顺序

  • 所以,在每⼀次事件循环的 tick中,会按照如下顺序来执⾏代码:

    • next tick microtask queue;

    • other microtask queue;

    • timer queue;

    • poll queue;

    • check queue;

    • close queue;

Node执⾏⾯试题

截屏2023-07-14 17.41.56