JavaScript(JS)本质上是单线程的,这意味着在任何时候,JavaScript引擎只能执行一个任务(或称作一个执行上下文)。这个特性决定了JS程序不能同时执行多个操作,特别是对于CPU密集型计算,它们必须按照顺序逐个执行。然而,尽管JS本身是单线程的,但它通过异步编程模型实现了并发效果,这种并发并非并行(parallelism),而是并发(concurrency),即任务之间看似同时发生但实际上在单线程中交错执行。
单线程限制与优势:
-
限制:由于只有一个执行线程,JS无法真正意义上同时处理多个任务。这意味着,当一个任务正在执行时,其他任务必须等待,尤其是那些耗时的操作(如网络请求、文件读写、长时间计算等),如果同步执行,将会阻塞主线程,导致用户界面无响应(UI freeze)。
-
优势:单线程简化了编程模型,避免了多线程编程中常见的复杂问题,如竞态条件(race conditions)、死锁(deadlocks)和资源同步。对于浏览器环境而言,这种设计有利于保护共享状态(如DOM),防止因多线程并发访问导致的混乱。
异步编程与事件循环:
-
异步编程:为了克服单线程带来的阻塞性,JavaScript采用了异步编程模型。异步操作(如网络请求、定时器、事件监听等)不会立即得到结果,而是会在未来某个时刻完成。JS提供了回调函数、Promise、async/await等机制来处理这些操作的结果,使得代码可以在不阻塞主线程的情况下等待异步任务完成。
-
事件循环:JavaScript实现异步并发的核心是事件循环(Event Loop)。事件循环是一个持续运行的机制,它负责管理任务队列(Task Queue)和微任务队列(Microtask Queue),并按特定规则调度这些任务的执行。
异步编程介绍:
同步任务:
console.log(123);
console.log(456);
for (let i = 1; i <= 5; i++) {
console.log(i);
}
这里一定是先输出123,再输出456,本质就是顺序执行。
异步任务:
setTimeout(() => {
console.log('定时器');
}, 0)
console.log('奥特曼');
setTimeout(() => { console.log('定时器'); }, 0) console.log('奥特曼');
按普通的执行顺序来说,定时器在上面 ,应该先输出定时器,再输出奥特曼
最后拿到的结果却先输出奥特曼 在输出了定时器 原因呢就是 setTimeout是异步任务。
对于宏任务和微任务 可以理解为两种异步的形态,异步有两个孩子宏任务和微任务。
宏任务中的方法:1. script (可以理解为外层同步代码,作为入口 ) 2. setTimeout/setInterval
微任务中的方法:1.Promise 2. nextTick
而他们的执行顺序是先输出微任务,再输出宏任务。
事件循环介绍:
由上文可知,先同步,再异步,异步中,先微任务,再宏任务。
如下代码的输出能体现出这个规则:
setTimeout(() => {
console.log('定时器');
}, 0)
new Promise((resolve) => {
console.log('同步代码')
resolve('异步代码')
}).then((res) => {
console.log(res);
})
console.log('奥特曼');
注意,new Promise是创建一个构造函数,这个过程是同步的,而.then方法是异步的。
顺序图如下:
执行栈:
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件,于是那些对应的异步任务结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步,称为事件循环(Event Loop)。
生动实例: