浏览器-宏任务与微任务
进程和线程
- 线程和进程是操作系统中的两个概念:
- 进程( process) :计算机 已经运⾏的程序 ,是 操作系统管理程序 的⼀种⽅式;
- 线程( thread) :操作系统能够运⾏ 运算调度的最⼩单位 ,通常情况下 它被包含在进程 中;
- 听起来很抽象,这⾥还是给出我的解释:
- 进程: 我们可以认为,启动 ⼀个应⽤程序 ,就会默认 启动⼀个进程 (也可能是多个进程);
- 线程: 每 ⼀个进程 中,都会启动 ⾄少⼀个线程 ⽤来执⾏程序中的代码,这个线程被称之为 主线程 ; 所以我们也可以说进程是线程的容器;
- 再⽤⼀个形象的例⼦解释:
- 操作系统类似于⼀个⼤⼯⼚;
- ⼯⼚中⾥有很多⻋间,这个⻋间就是进程;
- 每个⻋间可能有⼀个以上的⼯⼈在⼯⼚,这个⼯⼈就是线程;
操作系统 – 进程 – 线程
操作系统的⼯作⽅式
操作系统是如何做到同时让多个进程(边听歌、边写代码、边查阅资料)同时⼯作 呢?
- 这是因为 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()
宏任务和微任务
但是事件循环中并⾮只维护着⼀个队列,事实上是有两个队列:
- 宏任务队列( macrotask queue) : ajax、 setTimeout、 setInterval、 DOM监听、 UI Rendering等
- 微任务队列( microtask queue) : Promise的 then回调、 Mutation Observer API、 queueMicrotask()等
早期浏览器其实只有宏任务队列,并没有微任务队列,加入微任务队列是为了让某些代码更快执行
那么事件循环对于两个队列的优先级是怎么样的呢?
- 1.main script中的代码优先执⾏ (编写的顶层 script代码);
- 2.在 执⾏任何⼀个宏任务之前(不是队列,是⼀个宏任务) ,都会先查看微任务队列中是否有任务需要执⾏
- 也就是宏任务执⾏之前,必须保证微任务队列是空的;
- 如果不为空,那么就优先执⾏微任务队列中的任务(回调);
有一个方法,可以将任务加入微任务中去
- queueMicrotask()
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执⾏⾯试题