前端面试中,经常会被面试官问到:讲讲你对js事件循环机制的理解
我:事件循环就是js中用来处理异步任务和事件的一种机制。因为js是单线程的,需要这种机制去调用异步任务,防止非阻塞。。。。。
面试官继续问:具体的原理可以展开说说吗。。。怎么执行任务的。
我:嗯嗯,自己再去整理下这块内容,搞明白事件循环机制的具体执行原理吧
事件循环(Event Loop)
对事件循环的理解
事件循环机制是JS执行上下文中处理并发操作的一种方式。因为js是单线程的,但可以通过异步操作实现非阻塞行为,核心思想就是用一个调用栈去执行同步任务,对于异步任务不会在调用栈中立即执行,而是放入一个任务队列中。当调用栈为空时,事件循环会从任务队列中取出一个任务放入调用栈中执行,直到队列为空。整个的这种循环执行机制就称为事件循环
事件循环机制执行过程:
- 主线程上先执行同步任务,被压入调用栈,执行完毕后弹出。
- 遇到异步任务,先将异步任务先加入到任务队列
- 事件循环会不断检查调用栈是否为空,如果为空,就会从任务队列中取出一个任务放入调用栈中执行
- 首先检查微任务队列,如果微任务队列不为空,依次取出微任务 5.然后再检查宏任务队列,将宏任务放入调用栈中执行。执行完一个宏任务后,再次检查微任务队列
- 循环以上步骤,直到任务队列中所有任务都执行完毕
事件循环中的几个核心关键流程
调用栈(Call Stack) :
这是一个数据结构,用于存储同步任务的执行上下文。当函数被调用时,其执行上下文会被压入调用栈,执行完毕后弹出。
任务队列(Task Queue) :
分为宏任务队列(MacroTask Queue)和微任务队列(MicroTask Queue)。
宏任务和微任务
常见的宏任务:script,setTimeout,setInterval,I/O操作,postMessage,MessageChannel等;
常见的微任务:Promise.then,Object.observe, MutationObserver,process.nextTick等。
进程
进程是操作系统进行资源分配和调度的基本单位。拥有独立的内存空间,包括堆,栈。不同进程之间的内存是相互隔离的,保证一个进程崩溃不会影响其他进程。进程包含多个线程。
线程
线程是进程中的一个执行单元。 线程共享所属进程的资源,包括内存、文件描述符等。但每个线程有自己的程序计数器、栈和寄存器等。 多线程可以在同一个进程内并发执行,从而提高程序的执行效率。 比如,在一个浏览器进程中,渲染页面和下载资源可以在不同的线程同时进行。
主线程
在 JavaScript 中,主线程负责执行主要的程序逻辑。 主线程的主要职责包括:
-
执行 JavaScript 代码:例如函数的调用、变量的操作、控制流语句(如
if-else、for循环等)。 -
处理用户交互事件:比如鼠标点击、键盘输入等。
-
进行页面的渲染和更新。
当遇到异步操作时,如 setTimeout 、fetch 网络请求等,主线程不会被阻塞,而是会继续执行后续的代码。异步操作完成后,会将相应的回调函数放入任务队列中,等待主线程空闲时再执行。
事件循环机制的优点:
1. 提高程序响应性:
通过异步处理耗时操作,如网络请求、文件读取等,不会阻塞主线程的执行,使用户能够在这些操作进行的同时继续与界面或其他部分进行交互,提升用户体验。
2. 充分利用 CPU 资源:
避免了因为某个长时间运行的同步任务而导致 CPU 空闲等待。当一个异步任务被挂起等待结果时,CPU 可以处理其他任务,提高了 CPU 的利用率
3. 便于构建可扩展的系统:
由于事件驱动的特性,使得系统更容易添加新的功能和处理新的事件类型,具有良好的可扩展性。例如,在一个消息队列系统中,添加新的消息类型处理逻辑相对较为简单。
4. 跨平台一致性:
JavaScript 的事件循环机制在不同的平台(如浏览器、服务器端)上表现相对一致,减少了开发者为不同平台进行特殊处理的工作量。
所有的同步任务都是在主线程中执行,在主线程之外,有个任务队列,所有的异步任务都会进入任务队列。
JS中事件循环机制的优化方法:
1. 减少同步计算量
- 将复杂的计算任务拆分成小块,使用异步方式处理。
- 例如,使用
Web Workers在后台线程进行计算,避免阻塞主线程。
2. 优化异步回调函数
- 避免在回调函数中执行耗时操作。
- 保持回调函数简洁,将复杂逻辑提取到单独的函数中。
3. 控制任务队列长度
- 避免短时间内添加过多的异步任务到任务队列。
- 对频繁触发的事件进行防抖或节流处理。
4. 合理使用 Promise 和 async/await
Promise可以更好地管理异步流程,async/await使异步代码看起来更像同步代码,提高代码的可读性和可维护性。
5. 缓存和复用
- 对于重复的异步请求或计算结果进行缓存,避免不必要的重复操作。
6. 优化 DOM 操作
- 批量进行 DOM 操作,减少频繁的重绘和重排。
7. 资源加载优化
- 合理安排脚本、样式表和图片等资源的加载顺序和时机。
面试中常见的考察问题:
async function async1(){
console.log('async1 start');
await async2()
console.log('async1 end');
}
async function async2(){
console.log('async2 start');
}
console.log('script start');
setTimeout(()=>{
console.log('setTimeout');
},0)
async1()
new Promise((resolve)=>{
console.log('promise1');
resolve()
}).then(()=>{
console.log('promise2 then');
})
console.log('script end');
答案:
script start
async1 start
async2 start
promise1
script end
async1 end
promise2 then
setTimeout
解析:
代码执行顺序是先执行同步任务line10,输出script start
遇到setTimeout,会先将其加入到宏任务队列中
之后执行async1异步函数返回一个promise对象,promise对象是同步执行的,所以先输出async1 start
遇到await关键字,执行异步函数async2,先执行同步输出async2 start,然后将该任务加入到微任务队列
接着执行promise中的同步任务输出promise1,并将then函数加入到微任务队列中
继续执行同步任务输出script end,到此所有同步任务执行完毕
执行栈开始调用任务队列中的任务,先执行async2的异步,然后继续执行async1中后面的同步任务,输出async1 end
然后执行微任务队列中的then函数,输出promise2 then
最后执行宏任务中的setTimeout