JavaScript 事件循环
单线程的 JavaScript
JavaScript 是单线程的,这意味着它一次只能执行一个任务。然而,由于 JavaScript 的异步特性,它可以同时处理多个任务,而不会阻塞主线程。
- JavaScript 是单线程的,但是它通过事件循环机制实现了异步编程
- 事件循环机制包括两个主要部分:调用栈和任务队列
- 调用栈用于执行同步代码,任务队列用于处理异步代码
- 当一个异步操作完成时,它的回调函数会被添加到任务队列中,等待调用栈为空时执行
- 事件循环会不断检查任务队列,如果有任务就将其添加到调用栈中执行
- 事件循环机制使得 JavaScript 可以在等待异步操作完成的同时继续执行其他代码,从而实现非阻塞的异步编程
同步和异步
同步:代码按照顺序执行,一个任务执行完才会执行下一个任务
异步:代码可以同时执行多个任务,一个任务执行完不会等待其他任务执行完,而是继续执行下一个任务
JavaScript 本身是单线程的,为了处理异步任务,宿主环境(浏览器 /v8)会将其交给其他线程处理, 执行
事件循环
事件循环是宿主环境处理 js 异步操作的方式,让其能够非阻塞式运行的机制
-
浏览器进程
- 主进程,无论打开多少个浏览器窗口,它仅有一个,负责浏览器界面显示、用户管理、进程管理等
-
网络进程
- 处理网站的数据请求和响应,网络进程内部会开启多个线程,以实现网络请求的异步处理
-
渲染进程
- 主要解析 html、css、js 等资源,并生成渲染树、执行布局、绘制,负责页面渲染
浏览器中的 Event Loop
-
宏队列和微队列
- 宏队列排队宏任务(DOM 操作回调,定时器回调,UI 绘制)
- 微队列排队微任务(Promise 回调)
除了微队列外,队列的种类和数量可能不同,这取决与浏览器厂商
以 Chrome 为例:
- 微队列:用于存放需要执行最快的任务,优先级最高,比如:
promise.then()
,MutationObserve
- 交互队列:用于存放用户操作后产生的事件任务,优先级仅次于微队列
- 延迟队列:用于存放定时器到达后的回调任务,优先级次于交互队列
人工合成的事件派发,即直接在代码里写的 dom.click()
或dispatchEvent()
相对于浏览器而言并不是真正的用户交互,会被当作同步任务执行
执行栈和任务队列
JS 在解析一段代码的时候,会将同步代码顺序排在某个地方,即执行栈,然后依此执行里面的函数。
当遇到异步任务就交给其他线程处理,待当前执行栈所有的同步代码执行完成后,会从一个队列中取出已完成的异步任务的回调加入到执行栈中继续执行,遇到异步任务又交给其他线程......如此循环往复
宏任务和微任务
任务队列不止一个,根据任务的种类不同,可以分为微任务(micro task)队列和宏任务(macro task)队列
事件循环的过程中,执行栈在同步代码执行完成后,优先检查微任务队列中是否有任务需要执行,如果没有,再去宏任务队列检查,如此往复
微任务一般在当前循环就会优先执行,而宏任务会等到下一次循环
微任务一般比宏任务先执行,并且微任务队列只有一个,宏任务队列可能有多个
常见的宏任务
setTimeout()
setInterval()
setImmediate()
常见的微任务
Promise.then()
,Promise.cathch()
new MutaionObserver()
processs.nextTick()
js
console.info('同步代码1')
setTimeout(()={
console.info('setTimeot')
})
new Promise((resolve)=>{
console.info('同步代码2')
resolve()
}).then(()=>{
console.info('promise.then')
})
console.info('同步代码3')
宏任务和微任务的本质区别
对于 promise.then
(微任务),当执行到promise.then
的时候,浏览器引擎并不会将异步任务交给浏览器其他线程,而是将回调存放在自己的一个任务队列中,待当前执行栈执行完成后,立马去执行promise.then
存放的队列 promise.then
微任务本身没有多线程参与 setTimeout
有"定时等待"的任务,需要定时器现行执行。ajax 请求有"发送请求"这个任务,需要 HTTP 线程处理 宏任务特征: 有明确的异步任务需要执行回调,并且需要等待异步任务执行完成,比如setTimeout
,setInterval
,ajax
等 微任务特征: 没有明确的异步执行任务,不需要等待异步任务执行完成,比如promise.then
,process.nextTick
等
视图更新渲染
视图重绘之前会先执行 requestAnimationFrame
回调
浏览器 JS 异步执行的原理
浏览器是多线程的,当 js 需要执行异步任务的时候,浏览器会启动另一个线程去执行这个任务
js
const btn = document.querySelector("button");
function handleClick() {}
渲染进程启动后,会开启一个渲染主线程,它是浏览器中最繁忙的线程,负责处理各种任务
-
解析 html、css,计算样式、布局,构建 DOM 树和 CSSOM 树
-
处理涂层,绘制页面
-
执行 js 代码,包括同步代码和异步代码
-
调用栈(Call Stack)
- 用于执行同步代码,当调用栈为空时,事件循环会从任务队列中取出一个任务执行
-
任务队列(Task Queue)
- 用于存放异步任务,当异步任务完成时,它的回调函数会被添加到任务队列中,等待调用栈为空时执行
-
微任务队列(Microtask Queue)
Node.js 事件循环
基于 Libuv 实现的,Libuv 是一个跨平台的异步 I/O 库,它提供了事件循环、文件系统操作、网络操作等功能 Libuv: 一个用 C 语言实现的高性能解决单线程非阻塞异步 I/O 的开源库
Node.js Event Loop
-
宏队列
-
timers(重要)
-
penging callback
- 调用上一次事件循环没有在 pool 阶段立即执行,而延迟的 IO 回调函数
-
idle prepare
- 仅供 nodejs 内部使用
-
poll(重要)
-
check(重要)
-
close callback
- 执行所有注册 close 事件的回调函数
-
-
微队列
- nextTick
- Promise
timers
定时器队列,负责处理setTimeout
和setInterval
的回调函数 不管是nodejs
还是浏览器,所有的定时器回调函数都不能精准保证到达时间后立即执行
- 一是因为计算机硬件和底层操作系统
- 二是
pool
阶段对timers
阶段的深刻影响。因为在没有满足pool
阶段的结束条件前,就无法进入下一次事件循环的timers
阶段
Pool
pool
成为轮询队列,该阶段会处理timers
和check
队列外的绝大多数 IO 回调任务,比如文件读取、监听用户请求等 当事件循环到达该阶段,它的运行方式是:
-
如果
pool
队列中有回调任务,则依此执行回调,直到队列清空 -
如果
pool
队列中没有回调任务- 如果其他队列中后续可能会出现回调任务,则一直等待,等其他队列中后续的回调任务来临时,结束该阶段
- 如果等待的时间超过预设的时间限制,则也会自动进入下一次事件循环
- 若其他队列中后续不可能出现回调任务了,则立即结束该阶段,并在本轮事件循环完成后,退出
node
程序
js
const fs = require("fs");
const start = Data.now();
setTimeout(() => {
console.info("setTimeout exe", Data.now() - start);
}, 200);
fs.readFile("/index.js", "utf-8", (err, data) => {
console.info("file read");
const start = Data.now();
while (Data.now() - start < 300) {}
});
check
check
称为检查队列,负责处理setImmediate
定义的回调函数 setImmediate
是nodejs
特有的定时器,它会在当前事件循环的末尾执行回调函数,类似于setTimeout
,但setImmediate
的回调函数会在pool
阶段结束后立即执行 在nodejs
中,setImmediate
的执行效率远远高于setTimeout
,setImmediate
的执行顺序无法预测
js
setTimeout(() => {
console.info("setTimeout");
}, 0);
setImmediate(() => {
console.info("setImmediate");
}
nextTick
我们可以通过process.nextTick()
将回调函数加入到nextTick
队列中,和通过Promise.resolve().then()
将回调函数加入到Promise
队列,并且nextTick
队列中优先级高于Promise
队列。所有process.nextTick()
是 nodejs 执行最快的异步操作
Promise 面试
- Promise 特点
- 事件循环
- 解题思路
js
console.info("1");
new Promise((resolve) => {
resolve();
console.info("2");
}).then(() => {
console.info("3");
});
setTimeout(() => {
console.info("4");
}, 0);
console.info("5");
new Promise
会立即执行,所以会先输出2
resolve
或者reject
之后状态不再改变,但是后面代码会执行
js
new Promise((res) => {
res();
console.info("test");
reject();
});
promise
的then(catch)
回调放入到微任务队列,setTimeout
放入到宏任务队列- 调用栈中代码执行完后,先去微任务队列中的任务执行,直到微任务队列为空
- 微任务队列为空,取宏任务队列中的一个任务开始执行,然后重复上一步,直到宏任务队列为空