为什么js是单线程
初学计算机语言的时候,无论是 C、C++还是 JAVA,都是支持多线程,偏偏 JavaScript是单线程,不支持多线程,这也跟 JavaScript的作用有关,都知道 JavaScript是主要运行在浏览器的脚本语言,最终操作的是页面的 DOM结构,当两个 JavaScript脚本同时修改页面的同一个 DOM节点时,浏览器该执行哪个呢?所以当时设计 JavaScript时,便要求当前修改操作完成后方可进行下一步修改操作。
浏览器是支持多进程
浏览器的每一个 tab页都是一个进程,有对应的内存占用空间、 CPU使用量以及进程ID。 新打开一个 tab页时,都会新建一个进程,所以就有一个 tab页对应一个进程的说法,但是这种说法又是错误的,因为浏览器有自己的优化机制,当我们打开多个空白的 tab页时,浏览器会将这多个空白页的进程合并为一个,从而减少了进程的数量个数。
浏览器内核
浏览器内核中有多个进程在同步工作,今天涉及到的浏览器的进程主要包括以下进程:
- Browser 进程
- Render 进程
Browser 进程
主进程,主要负责页面管理以及管理其他进程的创建和销毁等,常驻的线程有:
- GUI渲染线程
- JS引擎线程
- 事件触发线程
- 定时器触发线程
- HTTP请求线程
GUI渲染线程
- 主要负责页面的渲染,解析HTML、CSS,构建DOM树,布局和绘制等。
- 当界面需要重绘或者由于某种操作引发回流时,将执行该线程。
- 该线程与JS引擎线程互斥,当执行JS引擎线程时,GUI渲染会被挂起,当任务队列空闲时,JS引擎才会去执行GUI渲染。
JS引擎线程
- 该线程当然是主要负责处理
JavaScript脚本,执行代码。- 也是主要负责执行准备好待执行的事件,即定时器计数结束,或者异步请求成功并正确返回时,将依次进入任务队列,等待
JS引擎线程的执行。- 当然,该线程与
GUI渲染线程互斥,当JS引擎线程执行JavaScript脚本时间过长,将导致页面渲染的阻塞。事件触发线程
- 主要负责将准备好的事件交给
JS引擎线程执行。- 比如
setTimeout定时器计数结束,ajax等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件依次加入到任务队列的队尾,等待JS引擎线程的执行。定时器触发线程
- 顾名思义,负责执行异步定时器一类的函数的线程,如:
setTimeout,setInterval。- 主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待JS引擎线程执行。
HTTP请求线程
- 顾名思义,负责执行异步请求一类的函数的线程,如:
Promise,anxios,ajax等。- 主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待JS引擎线程执行。
多个线程之间配合工作,各司其职。
Render 进程
浏览器渲染进程(浏览器内核),主要负责页面的渲染、JS执行以及事件的循环。
同步任务和异步任务
- 同步任务 即可以立即执行的任务,例如
console.log()打印一条日志、声明一个变量或者执行一次加法操作等。//PS:Promise里是同步操作!resolve&reject才是异步微事件!
- 异步任务 相反不会立即执行的事件任务。异步任务包括宏任务和微任务(后面会进行解释~)。
常见的异步操作:
- Ajax
- DOM的事件操作
- setTimeout
- Promise的then方法
- Node的读取文件

- 栈 就像是一个容器,任务都是在栈中执行。
- 主线程 就像是操作员,负责执行栈中的任务。
- 任务队列 就像是等待被加工的物品。
- 异步任务完成注册后会将回调函数加入任务队列等待主线程执行。
- 执行栈中的同步任务执行完毕后,会查看并读取任务队列中的事件函数,于是任务队列的函数结束等待状态,进入执行栈,开始执行。
宏任务和微任务
异步任务分为宏任务和微任务,宏任务队列可以有多个,微任务队列只有一个。
宏任务和微任务的执行方式在浏览器和 Node 中有差异。
宏任务(macrotask)
script(全局任务),setTimeout,setInterval,setImmediate,I/O,UI rendering
微任务(macrotask)
process.nextTick,Promise.then(),Object.observe,MutationObserver
当一个异步任务入栈时,主线程判断该任务为异步任务,并把该任务交给异步处理模块处理,当异步处理模块处理完打到触发条件时,根据任务的类型,将回调函数压入任务队列。
- 如果是宏任务,则新增一个宏任务队列,任务队列中的宏任务可以有多个来源。
- 如果是微任务,则直接压入微任务队列。
所以上图的任务队列可以继续细化一下:

Event Loop
到这里,除了上面的问题,我们已经把事件循环的最基本的处理方式搞清楚了,但具体到异步任务中的宏任务和微任务,还没有弄明白。我们可以先顺一遍执行机制:
- 从全局任务
script开始,任务依次进入栈中,被主线程执行,执行完后出栈。 - 遇到异步任务,交给异步处理模块处理,对应的异步处理线程处理异步任务需要的操作,例如定时器的计数和异步请求监听状态的变更。
- 当异步任务达到可执行状态时,事件触发线程将回调函数加入任务队列,等待栈为空时,依次进入栈中执行。
到这问题就来了,当异步任务进入栈执行时,是宏任务还是微任务呢?
- 由于执行代码入口都是全局任务
script,而全局任务属于宏任务,所以当栈为空,同步任务任务执行完毕时,会先执行微任务队列里的任务。 - 微任务队列里的任务全部执行完毕后,会读取宏任务队列中拍最前的任务。
- 执行宏任务的过程中,遇到微任务,依次加入微任务队列。
- 栈空后,再次读取微任务队列里的任务,依次类推。
console.log(1);
setTimeout(function(){
console.log(2);
new Promise(function(resolve, reject){
console.log(3);
resolve();
}).then(function(){
console.log(4);
})
})
new Promise(function(resolve, reject){
console.log(5);
resolve();
}).then(function(){
console.log(6);
})
setTimeout(function(){
console.log(7)
})
setTimeout(function(){
console.log(8);
new Promise(function(resolve, reject){
console.log(9);
resolve();
}).then(function(){
console.log(10);
})
})
new Promise(function(resolve){
console.log(11);
resolve();
}).then(function(){
console.log(12)
})
console.log(13)
// 答案
1
5
11
13
6
12
2
3
4
7
8
9
10