我们知道JavaScript是一种同步的、阻塞的、单线程的语言,这篇文章的内容就是介绍JavaScript事件循环。
同步任务和异步任务
好比一个人要吃饭,肯定是要经过洗手然后吃饭的步骤,他自己没有办法在洗手的同时还能吃饭。但是在做饭的时候一般都是煮饭和炒菜同时进行,而不是一定要盯着电饭锅直到把饭煮好才能炒菜。这种一边煮饭一遍炒菜的方式我们称之为异步。
同步任务:指在主线程上执行的任务,其特点是只有当前一个任务执行完成才能执行下一个任务。
异步任务:由JS委托给宿主环境执行,等执行完成之后再通知主线程执行异步任务的回调函数。
线程与进程
进程:进程是对正在运行中的程序的一个抽象,是系统进行资源分配和调度的基本单位,操作系统的其他所有内容都是围绕着进程展开的,负责执行这些任务的是cup。
线程:线程是操作系统能够进行运算调度的最小单位,其是进程中的一个执行任务(控制单元),负责当前进程中程序的执行。
举个例子:进程=火车,线程=车厢
- 线程在进程下行进(单纯的车厢无法运行)
- 一个进程可以包含多个线程(一辆火车可以有多个车厢)
- 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
- 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
- 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
浏览器多线程
浏览器是JavaScript的宿主之一,他有着很多进程,其中的**渲染进程(Renderer Process)**就是我们平时用于执行JavaScript的进程。渲染进程中又有好几个线程,这些线程相互协调,我们的js代码便良好地运行起来了。
GUI渲染线程
- GUI线程负责渲染页面,解析HTML和CSS,构建成DOM树和渲染树。
- 界面重绘和回流也会执行
- 该线程JS引擎线程是互斥的
JS引擎线程(JavaScript Engine Thread)
- JS的引擎,负责处理Javasc的代码(如V8引擎)。
- JavaScript说的单线程指的就是该线程
- JS引擎线程执行是GUI渲染线程会被挂起(他们是互斥的)
主线程(Main Thread)
- 浏览器的线程,存在一个事件队列,用于控制事件循环。
- 当js引擎执行异步任务时会将这些任务加入事件队列等js引擎空闲时候处理
定时触发器线程(Timer Thread)
- 执行
setInterval
与setTimeout
的线程 - 定时器由浏览器的定时触发器线程计时,计时完毕后添加到事件队列中(放入事件触发线程中)
- W3C在HTML标准中规定要求
setTimeout
中低于4ms的时间间隔为4ms(也就是0ms也算4ms)
异步HTTP请求线程
- 在XMLHttpRequest连接后是通过浏览器新开的一个线程请求
- 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中(放入事件触发线程中)。再由JavaScript引擎执行
Web Worker 线程:
- 用于执行长时间运行的JavaScript代码,但是与主线程和 GUI 渲染线程是完全独立的,不会阻塞主线程的执行
什么是事件循环Event Loop?
事件循环(Event Loop)是一种用于处理异步任务和事件的机制,常见于浏览器环境和Node.js环境中。它是使得 JavaScript 能够处理非阻塞异步操作的核心机制,使代码能够按照一定的顺序执行,同时处理异步任务,而不会阻塞主线程的执行。
我们知道,JavaScript是一种同步的、阻塞的、单线程的语言,那这个意思就是js的代码只能是一行一行执行下来的,不过有意思的是由于setTimeout
、promise
等异步任务,我们可以先执行一部分代码,然后再执行异步任务里的代码。JS的代码执行方式如下:
setTimeout
、promise
等回调函数虽然都是异步任务,同样的代码无论setTimeout
是在promise
前面还是后面,setTimeout
返回的结果总是会比promise
慢,这是怎么一回事呢?要分析这个问题,我们就需要引入宏任务和微任务这两个概念了。
宏任务(macroTask)和微任务(microTask)
我们把宿主环境提供的任务称为宏任务,把由 JavaScript 引擎自身提供的任务称为微任务。并且微任务优先级比宏任务更高。
宏任务主要有:
- setTimeout 和 setInterval:用于在一定时间后执行回调函数或定期执行回调函数
- XMLHttpRequest 和 Fetch:用于进行网络请求,获取数据
- DOM事件:例如点击事件、输入事件等。
- requestAnimationFrame:用于在下一次重绘之前执行回调函数,通常用于动画效果。
- 页面渲染:当浏览器需要重新渲染页面时触发的任务。
- 文件读写:例如使用 FileReader 进行文件读取。
- 文件 I/O(Nodejs):例如读写文件。
- 网络 I/O(Nodejs):例如进行网络请求,与外部服务器进行通信。
微任务主要有:
- Promise.then(非 new Promise):当 Promise 状态变为 resolved 或 rejected 时,会产生微任务。
- MutationObserver:用于监听 DOM 树的变化,一旦发现 DOM 变化,会产生微任务。:用于监听 DOM 树的变化,一旦发现 DOM 变化,会产生微任务。
- process.nextTick(Nodejs): 用于在当前执行栈结束后立即执行回调函数。
关于setTimeout 和 setInterval
- setTimeout: 用于在一段时间后执行一次指定的函数或代码块。
- setInterval: 用于以固定的时间间隔重复执行指定的函数或代码块。
setInterval的隐患
二者的作用都是在间隔某段时间后去执行指定的函数,但是不同的是setTimeout
只执行一次,而setInterval
却是每隔这一个时间间隔都执行一次,直到收到取消指令,如果一个定时任务的执行时间比时间间隔本身长,会累积执行时间的误差,而且还会造成任务堆积。
JavaScript
let startTime = +new Date()
setInterval(() => {
console.log('间隔时间:'+ (+new Date() - startTime))
const count = Math.floor(10000000000 * Math.random())
for (let i = 0;i < count;i++) { }
startTime = +new Date()
}, 1000)
处理方式
基于setInterval
存在的隐患,我们可以试着用setTimeout
去实现setInterval
,这样可以避免这种误差,并且保证执行完任务才重新开启宏任务,不会造成任务堆积。
JavaScript
let startTime = +new Date()
function doTask() {
setTimeout(() => {
console.log('间隔时间:'+ (+new Date() - startTime))
const count = Math.floor(10000000000 * Math.random())
for (let i = 0;i < count;i++) { }
startTime = +new Date()
doTask()
}, 1000)
}
doTask()
forEach中的异步
我们有时候可能会接到一些需求,要循环调用几个接口,并且他们是有先后顺序的,我们会想到用forEach
去解决这个问题,但是当我们使用的时候就会发现,循环的结果与我们想要的不一致。
JavaScript
const urls = ['https://juejin.cn','https://www.zhihu.com', 'https://baidu.com'];
urls.forEach(async (url) => {
const response = await fetch(url);
console.log(response);
});
forEach无法使用async/await的原因
forEach
方法会在当前作用域内同步执行回调函数,不会创建一个新的作用域,因此不会等待异步操作完成,而是继续迭代,尽管使用了async/await
,但它们是无效的。
解决方案
for...of
循环和 for...in
循环以及 for
循环都可以使用 async/await
,因此我们可以使用它们替代forEach
。
JavaScript
const urls = ['https://juejin.cn','https://www.zhihu.com', 'https://baidu.com'];
for (const url of urls) {
const response = await fetch(url);
console.log(response);
}
JavaScript中的"多线程"
HTML5中提出了Web Worker
,它允许在 JavaScript 中创建多线程环境,从此出现JavaScript不再是单线程的说法开始出现。其实这个说法是错的,因为JavaScript一直以来都是单线程的脚本语言,Web Worker
只是创建了一个线程环境,并运行在这个线程中运行JavaScript,并且该线程不会影响到主线程,因而看起来像是JavaScript变成了多线程而已。
最后
最后我们举一道经典的异步题目,如果能自己算出这道题,那么说明你对事件循环的理解已经可以了。如果算的结果和代码运行的结果不一致,那这里就是你的短处,需要再理解一遍我们的知识点。
JavaScript
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
Promise.resolve().then(() => {
console.log(6)
}).then(() => {
console.log(7)
setTimeout(() => {
console.log(8)
}, 0);
});
})
setTimeout(() => {
console.log(9)
}, 0);
console.log(10)