JavaScript 语言的一大特点是单线程。
线程和进程
- 线程是 cpu 调度的最小单位(线程是进程上的一次程序运行单位,一个进程可以有多个线程)。
- 进程是 cpu 资源分配的最小单位(是能拥有资源和独立运行的最小单位)。
浏览器是多进程的
每开一个标签页系统就创建一个独立的进程,一个进程中可能包含如渲染线程、JS 引擎线程、HTTP 请求线程等。
注意:由于浏览器的优化机制某些进程可能会被合并。
为什么JavaScript是单线程的
如果 JavaScript 是多线程的,假定 procces1 和 procces2 同时对同一个 DOM 作出操作,浏览器应该以哪一个为准,所以 JavaScript 只能为单线程。
事件循环
任务分为同步任务和异步任务,同步任务会进入主线程,异步任务则会进入事件队列 ( Event Queue )。主线程中的任务执行完毕后会从事件队列中取出放入执行栈。
任务除了分为同步任务和异步任务,还分为微任务和宏任务。
- 微任务(microtask)包括:process.nextTick,Promise,MutationObserver
- 宏任务(macrotask)包括:主代码script,setTimeout,setInterval
事件循环的执行顺序为
- 事件循环开始,script 代码块做为宏任务进入主线程执行
- 同步任务移入主线程执行,异步任务进入事件队列
- 主线程执行完成后,取出事件队列中的微任务执行
- 微任务执行完毕后,事件循环结束
- 取出事件队列中的宏任务,开始新一轮事件循环
- ......
示例:
setTimeout(() => {
new Promise((resolve) => {
console.log('1')
resolve()
}).then(() => {
console.log('2')
})
console.log('3')
}, 0)
console.log('4')
new Promise(resolve => {
console.log('5')
resolve();
}).then(() => {
console.log('6')
})
setTimeout(() => {
new Promise((resolve) => {
console.log('7')
resolve();
}).then(() => {
console.log('8')
})
console.log('9')
}, 0)
分析:
-
第一轮事件循环:
- script 作为宏任务进入主线程
- 遇到 setTimeout,注册其回调函数作为宏任务移入事件队列,记为 setTimeout1
- 遇到 console.log,输出'4'
- 遇到 Promise,输出'5',注册其回调函数作为微任务进入事件队列,记为 then
- 遇到 setTimeout,注册其回调函数作为宏任务移入事件队列,记为 setTimeout2
- 宏任务执行完毕,执行事件队列中的微任务 then,输出'6'
第一轮事件循环结束输出'4'、5'、'6'
-
第二轮事件循环:
- 执行宏任 setTimeout1
- 遇到 Promise,输出'1',注册其回调函数作为微任务进入事件队列,记为 then
- 遇到 console.log,输出'3'
- 宏任务执行完毕,执行事件队列中的微任务 then,输出'2'
第二轮事件循环结束输出'1'、'3'、'2'
-
第三轮事件循环:
- 遇到 Promise,输出'7',注册其回调函数作为微任务进入事件队列,记为 then
- 遇到 console.log,输出'9'
- 宏任务执行完毕,执行事件队列中的微任务 then,输出'8'
第三轮事件循环结束输出'7'、'9'、'8'
代码执行完毕输出结果为'4'、5'、'6'、'1'、'3'、'2'、'7'、'9'、'8'
参考:前端面试之道