Skip to content

首先,JavaScript 是单线程语言。

1. 进程和线程的关系

一个程序至少有一个进程,而一个进程至少有一个线程。线程是进程的执行单元,是 CPU 调度和分派的基本单位,可以看成实际在干活运算的是线程,而进程只是一个或多个线程的资源分配和调度的系统单位。

如果把进程比作一个工厂,线程则是工厂里干活的工人。工人共享一个劳动空间,共享劳动工具。也就是说,在一个进程里,有一个或多个线程,各线程共享同一个内存空间和数据。

可以看看阮一峰的《进程与线程的一个简单解释》

2. 浏览器是多线程

  • GUI 渲染线程
  • JS 引擎线程
  • 事件触发线程
  • 定时触发线程
  • 异步 HTTP 请求线程

其中,GUI 线程和 JS 线程是互斥的。这也就是为什么某些页面在执行 JS 运算时会导致 DOM 刷新卡住。因为当 JS 线程在执行运算时,GUI 线程会被挂起。

早期通过在代码中增加 setTimeout 来解决渲染卡顿问题。

3. JavaScript 是单线程

JavaScript 可以同时执行多个 JS 文件代码,看似多线程同时执行,其实还是单线程,只不过是分隔成多个任务,在不同任务之间跳转进行运算。

JavaScript 的执行机制称为 Event Loop。任务分为同步和异步。

同步任务都在主线程上执行,遇到异步任务则丢到任务队列,接着继续执行同步任务。当同步任务执行完毕,主线程空闲后,才会回过头去查看任务队列。

event loop (图片来源于谷歌,描述了任务在事件循环中的执行顺序)

这也就是 setTimeout(() => {}, 0) 并不会立马执行,延迟时间并不准确的原因,因为主线程还没空闲去执行任务队列。

js
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 执行时间太长造成页面假死。

微任务则是在渲染之前执行。简单来说,在主代码执行完后,执行渲染,然后再去查看任务队列。

但是如果有微任务,则是执行主代码后,先执行微任务,然后渲染,再执行任务队列。

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 之前执行的。网上有很多示例代码,套了很多层,实际工作中并不常用到。

上次更新于: