1. JavaScript的单线程模型
JavaScript是一种单线程的语言,意味着它一次只能执行一个任务。想象一下,JavaScript就像一个工人,他只能同时做一件事情。如果他在做一个任务时,还想处理别的事情,就只能等前一个任务完成后再做。
2. 异步编程的需求
但在现代Web开发中,我们通常需要处理多个任务,比如:
- 从服务器获取数据
- 等待用户点击按钮
- 定时执行某些操作
为了避免让JavaScript在等待这些操作时阻塞其他任务,JavaScript引入了“异步编程”模型,这样它可以在等待某些操作(如网络请求)时继续执行其他任务。
3. 事件循环 (Event Loop)
事件循环是JavaScript处理并发任务的机制。简单来说,事件循环帮助JavaScript管理如何执行异步任务。
JavaScript的运行机制可以分为以下几个部分:
- 调用栈(Call Stack):调用栈是一个存放任务的地方,JavaScript会把当前正在执行的代码放在栈上。代码执行完成后,就会从栈中弹出。
- 消息队列(Message Queue):当异步任务(如
setTimeout、Promise)完成后,它的回调函数会被放入消息队列中。事件循环会不断地查看调用栈是否为空,如果为空,就从消息队列中取出回调函数执行。
4. 同步任务与异步任务
- 同步任务:这些任务是按照顺序执行的,一个任务完成后才会执行下一个任务。例如,普通的函数调用就是同步任务。
- 异步任务:异步任务是一些不需要立即完成的任务,例如
setTimeout、网络请求、文件读取等。JavaScript不会等待这些任务完成,而是继续执行后面的代码。完成后,它们的回调函数会被放入消息队列等待执行。
5. 事件循环的工作流程
事件循环的核心思想是:JavaScript在处理完所有的同步任务后,会去执行异步任务。事件循环大致是按以下步骤工作的:
- 执行同步代码:首先,JavaScript执行调用栈中的所有同步任务,直到栈为空。
- 执行微任务:微任务包括
Promise的回调函数,它们的优先级高于宏任务。当调用栈空闲时,事件循环会首先检查微任务队列,并执行所有的微任务。 - 执行宏任务:宏任务包括
setTimeout、用户点击事件等。当微任务队列为空时,事件循环才会从宏任务队列中取出任务执行。 - 重复循环:事件循环会不断重复这个过程。
6. 举个例子
假设我们有以下代码:
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");
代码执行顺序:
console.log("Start")会立即执行,输出:Start。setTimeout是一个异步操作,尽管设置了0毫秒的延时,但它是一个宏任务,因此回调函数会被加入到宏任务队列中。Promise.resolve().then()是微任务,它的回调函数会被加入到微任务队列中。console.log("End")会立即执行,输出:End。- 事件循环检查微任务队列,发现有
Promise的回调任务,执行该回调,输出:Promise。 - 最后,事件循环会去宏任务队列中取出
setTimeout的回调任务,执行,输出:Timeout。
最终输出:
Start
End
Promise
Timeout
7. 宏任务与微任务的区别
- 宏任务:
setTimeout、setInterval、I/O操作(如网络请求),这些任务会被放入宏任务队列。 - 微任务:
Promise、MutationObserver等,这些任务的优先级高于宏任务,会先于宏任务执行。
8. 为什么要了解事件循环
理解事件循环非常重要,尤其是在处理异步操作时。它帮助我们控制异步任务的执行顺序,避免任务阻塞,提升程序的性能和响应速度。
关于 JavaScript并发模型 和 事件循环 的面试题。
1. JavaScript中的并发模型是什么?
JavaScript是**单线程**的编程语言,这意味着它一次只能执行一个任务。然而,为了处理异步任务(如网络请求、定时器等),JavaScript引入了**事件循环**机制,通过这种机制来模拟并发执行。
事件循环通过将异步任务分为两类(宏任务和微任务),并利用 消息队列 和 调用栈 来逐步执行这些任务。事件循环会首先处理同步任务,然后依次处理微任务,最后处理宏任务。
2. 什么是事件循环(Event Loop)?
事件循环是JavaScript处理异步操作的机制。由于JavaScript是单线程的,它无法同时执行多个操作,但通过事件循环,它可以模拟并发操作。
事件循环的工作方式如下:
- 同步代码会先执行。同步任务会被依次加入调用栈(Call Stack)并执行,直到调用栈为空。
- 微任务队列会在调用栈清空后立即执行所有微任务(如
Promise的回调函数)。 - 宏任务队列(如
setTimeout、I/O操作)会在微任务执行完毕后才会执行。
3. JavaScript中的宏任务和微任务有什么区别?
-
**宏任务(Macrotasks)**是较为重的任务,它们包括:
setTimeout/setInterval- DOM事件处理程序
- I/O操作等
宏任务会被放入宏任务队列(Macrotask Queue)中,等待事件循环处理。
-
**微任务(Microtasks)**是较轻的任务,它们包括:
Promise的then/catch/finally回调MutationObserver
微任务的优先级高于宏任务,事件循环在处理完所有同步代码后,首先会执行微任务队列中的任务,然后才会执行宏任务队列中的任务。
4. 解释一下下面这段代码的执行顺序:
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('End');
执行顺序:
console.log('Start')立即执行,输出:Start。setTimeout是一个宏任务,虽然设置了0毫秒的延时,它的回调函数会被加入到宏任务队列中。Promise.resolve().then()是一个微任务,它的回调函数会被加入到微任务队列中。console.log('End')立即执行,输出:End。- 事件循环检查微任务队列,发现有一个微任务(
Promise),执行该回调,输出:Promise。 - 最后,事件循环去宏任务队列中取出
setTimeout的回调任务,执行它,输出:Timeout。
最终输出顺序:
Start
End
Promise
Timeout
5. 为什么JavaScript的事件循环是必要的?
JavaScript的事件循环机制使得它能够在单线程环境下处理异步任务。如果没有事件循环,JavaScript就无法高效地处理像用户交互、网络请求、文件读取等这些需要等待的任务。事件循环通过消息队列和微任务队列管理异步任务,让JavaScript能够在同步代码执行完后,优先执行微任务,再执行宏任务,从而避免了长时间的阻塞。
6. 在JavaScript中,setTimeout 和 setImmediate 有什么区别?
-
setTimeout(callback, 0):虽然设置的时间是0毫秒,但它的回调函数会被加入到宏任务队列中。在事件循环的下一次迭代时执行。 -
setImmediate(callback):这是Node.js中的API,它的回调函数会立即被放入轮询阶段的回调队列中,优先级高于宏任务,但低于微任务。总结:在浏览器中没有
setImmediate,只有setTimeout。在Node.js中,setImmediate和setTimeout(时间为0)都属于宏任务,但setImmediate会在当前事件循环结束后立即执行,而setTimeout会等待下一轮事件循环。
7. Promise的微任务队列与setTimeout的宏任务队列的执行顺序是什么?
Promise的回调会被放到微任务队列中,而setTimeout的回调会被放到宏任务队列中。
执行顺序:
- 执行同步代码。
- 执行所有微任务(如
Promise回调)。 - 执行宏任务(如
setTimeout回调)。
即使setTimeout设置为0毫秒,Promise的回调依然会优先执行。
8. 事件循环中“调用栈”和“消息队列”的作用是什么?
- 调用栈(Call Stack):它存储着正在执行的函数。JavaScript代码会从调用栈中逐个执行,当一个函数执行完毕时,会从栈中弹出,继续执行下一个任务。
- 消息队列(Message Queue):存放着异步任务的回调(如
setTimeout、Promise的then回调等)。当调用栈空闲时,事件循环会从消息队列中取出任务并执行。
9. 在什么情况下,JavaScript的事件循环会“阻塞”?
JavaScript的事件循环会被“阻塞”当有长时间运行的同步代码时。例如:
- 一个大的计算循环(如大量的
for循环),它会一直占用调用栈,直到执行完毕,导致消息队列中的异步任务无法得到执行。 - 执行一个阻塞I/O操作时,JavaScript会等待I/O操作完成后才能继续执行后续的任务。
为了避免阻塞,可以通过将长时间运行的代码拆分为多个小任务,或者使用setTimeout/setImmediate等异步方法来分批执行。
10. 如何解决事件循环中的"任务阻塞"问题?
可以通过以下方式避免或减轻任务阻塞:
- 将长时间运行的任务拆分成小任务:使用
setTimeout或requestAnimationFrame将一个大任务分解成多个小任务,这样它们会被逐个加入到消息队列中,防止阻塞主线程。 - 使用Web Workers:对于计算密集型的任务,可以使用Web Workers将任务放到后台线程,避免阻塞主线程。