首先,JavaScript 是单线程语言。
1. 进程和线程的关系
一个程序至少有一个进程,而一个进程至少有一个线程。线程是进程的执行单元,是 CPU 调度和分派的基本单位,可以看成实际在干活运算的是线程,而进程只是一个或多个线程的资源分配和调度的系统单位。
如果把进程比作一个工厂,线程则是工厂里干活的工人。工人共享一个劳动空间,共享劳动工具。也就是说,在一个进程里,有一个或多个线程,各线程共享同一个内存空间和数据。
可以看看阮一峰的《进程与线程的一个简单解释》。
2. 浏览器是多线程
- GUI 渲染线程
- JS 引擎线程
- 事件触发线程
- 定时触发线程
- 异步 HTTP 请求线程
其中,GUI 线程和 JS 线程是互斥的。这也就是为什么某些页面在执行 JS 运算时会导致 DOM 刷新卡住。因为当 JS 线程在执行运算时,GUI 线程会被挂起。
早期通过在代码中增加 setTimeout
来解决渲染卡顿问题。
3. JavaScript 是单线程
JavaScript 可以同时执行多个 JS 文件代码,看似多线程同时执行,其实还是单线程,只不过是分隔成多个任务,在不同任务之间跳转进行运算。
JavaScript 的执行机制称为 Event Loop。任务分为同步和异步。
同步任务都在主线程上执行,遇到异步任务则丢到任务队列,接着继续执行同步任务。当同步任务执行完毕,主线程空闲后,才会回过头去查看任务队列。
(图片来源于谷歌,描述了任务在事件循环中的执行顺序)
这也就是 setTimeout(() => {}, 0)
并不会立马执行,延迟时间并不准确的原因,因为主线程还没空闲去执行任务队列。
setTimeout(() => {
console.log('哟哟哟')
}, 1000)
执行时,并不是说放到任务队列,1000ms 后执行,而是由定时器线程在 1000ms 后才丢到队列!常用的 setInterval 也是一样的逻辑。
4. 宏任务与微任务
宏任务 (macrotask),包括主线程、setTimeout、setInterval、requestAnimationFrame。
微任务 (microtask),包括 Promise、process.nextTick、MutationObserver 以及已废弃的 Object.observe。
前面提到通过在代码中增加 setTimeout 来解决渲染卡顿问题,因为 JS 线程和 GUI 线程互斥,所以执行的顺序实际上是这样:
JS 任务 => 渲染 DOM => JS 任务 => 渲染 DOM
通过 setTimeout 把长运算代码切片处理,防止 JS 执行时间太长造成页面假死。
微任务则是在渲染之前执行。简单来说,在主代码执行完后,执行渲染,然后再去查看任务队列。
但是如果有微任务,则是执行主代码后,先执行微任务,然后渲染,再执行任务队列。
console.log('begin');
setTimeout(function() {
console.log('setTimeout');
}, 0);
new Promise((resolve) => {
console.log('promise');
resolve();
}).then(() => {
console.log('promise.resolve');
});
console.log('end');
执行结果:
begin
promise
end
promise.resolve
setTimeout
可以看出 Promise.resolve
是在 setTimeout
之前执行的。网上有很多示例代码,套了很多层,实际工作中并不常用到。