单线程的JavaScript如何应对多任务?
JavaScript的世界是一个单线程的世界。这意味着,它一次只能做一件事。然而,现代Web应用的需求却远远超出了“一件事”的范畴:网络请求、用户交互、动画渲染……这些任务都需要同时进行。那么,JavaScript是如何在单线程的限制下,高效地处理这些任务的呢?
答案就是——事件循环(Event Loop)。它是JavaScript世界的“时间管理大师”,通过巧妙的调度机制,让单线程的JavaScript能够同时处理多个任务。
一、进程与线程——浏览器中的“工厂”与“工人”
1. 进程:浏览器的“工厂”
每当我们打开一个新的浏览器标签页,浏览器就会为我们创建一个新的进程。你可以把进程想象成一个独立的工厂,每个工厂都有自己的资源和工人(线程)。这些工厂的任务是完成页面的加载和渲染。
2. 线程:工厂里的“工人”
在工厂里,工人们(线程)分工明确:
- 渲染线程:负责将HTML、CSS转换成用户看到的页面。
- JS引擎线程:负责执行JavaScript代码。
- HTTP线程:负责发送网络请求,获取数据。
然而,JS引擎线程和渲染线程是互斥的,也就是说,他们不能同时工作。这是因为JavaScript可以操作DOM,而渲染线程也会操作DOM。如果两者同时工作,就会导致冲突。因此,当JS引擎线程在执行任务时,渲染线程必须等待。
二、事件循环——JavaScript的“时间管理大师”
1. 同步任务:按部就班的工作
JavaScript的执行从同步任务开始。这些任务就像工人的日常工作清单,按顺序执行,不可打断。例如:
console.log("开始工作");
console.log("完成任务");
这段代码会先输出“开始工作”,再输出“完成任务”,顺序明确,不可更改。
2. 异步任务:巧妙的“分身术”
然而,有些任务非常耗时,比如网络请求或定时器。如果工人一直等着这些任务完成,其他任务就无法进行了。于是,工人学会了“分身术”——异步任务。他会把这些耗时任务先放到一边,也可以说放入了其他队列,自己则继续执行同步代码。
setTimeout(() => {
console.log("定时任务完成");
}, 1000);
console.log("继续工作");
这段代码中,工人会先把定时任务挂起,放到其他任务队列中,等待事件循环处理,自己则继续“继续工作”,执行同步代码。
三、异步任务:微任务与宏任务
异步任务分为微任务与宏任务。
1. 微任务
在任务队列中,常见的微任务包括:
Promise.then()process.nextTick()MutationObserver
事件循环会先执行所有微任务,然后再执行宏任务。例如:
Promise.resolve().then(() => {
console.log("微任务执行");
});
setTimeout(() => {
console.log("宏任务执行");
}, 0);
这段代码会先输出“微任务执行”,再输出“宏任务执行”。
2. 宏任务
宏任务包括:
scriptsetTimeoutsetIntervalsetImmediateI/O操作UI渲染
事件循环会在执行完所有微任务后,再执行宏任务。之后循环往复。
四、事件循环的步骤——JavaScript的“时间管理法则”
事件循环的工作步骤如下:
- 执行同步代码:这是事件循环的第一优先级。(同时也是一次宏任务的开始)
- 检查并执行微任务:所有微任务会在同步代码执行完毕后立即执行。
- 渲染页面:如果需要,事件循环会触发页面渲染。
- 执行宏任务:宏任务的执行标志着下一次事件循环的开始。
执行宏任务也是开启了下一次事件循环,这个过程就像一个永不停止的循环,确保JavaScript能够高效地处理各种任务。
五、await的“提前执行”与setTimeout的“时间误差”
1. await的“提前执行”
await后面的代码要当成同步执行,并将后续代码“挤入”微任务队列。例如:
async function test() {
console.log("开始");
await Promise.resolve();
console.log("结束");
}
test();
这段代码会先输出“开始”,随后执行await Promise.resolve(),之后将“结束”放入微任务队列,等待执行。
2. setTimeout的“时间误差”
setTimeout的定时器并不总是准确的。当定时器的时间到达时,如果JS主线程还在执行同步代码或微任务,定时器的回调会被挂起,直到主线程空闲。因此,setTimeout的时间只是一个“最小等待时间”,而不是“精确时间”。
六、小练笔
请看接下来这段代码
console.log('script start'); //1 输出script start
async function async1() {
await async2() //3 执行async2() //await后面的代码要当成同步执行
console.log('async1 end'); //await下面的代码去到微任务队列
//7先进入微任务队列,先输出async1 end
}
async function async2() {
console.log('async2 end'); //4 输出async2 end
}
async1() //2 此时调用async1()
setTimeout(() => { // 去到宏任务队列 //10 输出setTimeout
console.log('setTimeout');
}, 0)
new Promise((resolve, reject) => { //注意:后面接的.then才是微任务
console.log('promise'); //5 输出promise
resolve()
})
.then(() => {
console.log('then1'); // 去到微任务队列 //8 输出then1
})
.then(() => {
console.log('then2'); // 去到微任务队列 //9 输出then2
});
console.log('script end'); //6 输出script end
输出如下:
1-6同步任务执行完毕,接下来执行微任务
7-9执行微任务
10执行宏任务
//同步
script start
async2 end
promise
script end
//微任务
async1 end
then1
then2
//宏任务
setTimeout