JavaScript 是单线程的
JS 是一门单线程的非阻塞的脚本语言,这表示在同一时刻最多也只有一个代码段执行。
为什么 JavaScript 是单线程的
如果 JS 是多线程的,因为 JS 有 DOM API 可以操作 DOM,如果同时开了两个线程同时操作 DOM 的话,一个线程删除了当前的 DOM 节点,另一个线程要操作当前的 DOM,那么就会有矛盾到底以哪个线程为主。为了避免这种情况出现,JS 就被设计为单线程,而且单线程执行效率高。现在虽然也有 web worker 标准的出现,但它也有很多限制,受主线程控制,是主线程的子线程。
JS 如何处理异步任务
JS 是单线程,那么非阻塞怎么体现呢?如果 JS 是阻塞的,那么 JS 发起一个异步 IO 请求,在等待结果返回的这个时间段,后面的代码就无法执行了,而 JS 主线程和渲染进程是互斥的,因此可能造成浏览器假死的状态。事实 JS 是非阻塞的,那它要怎么实现异步任务呢,靠的就是事件循环。
事件循环
事件循环就是通过异步执行任务的方法来解决单线程的弊端的。
- 一开始整个脚本作为一个宏任务执行
- 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
- 当前宏任务执行完出队,读取微任务列表,有则依次执行,直到全部执行完
- 执行浏览器 UI 线程的渲染工作
- 检查是否有 Web Worker 任务,有则执行
- 执行完本轮的宏任务,回到第 2 步,继续依此循环,直到宏任务和微任务队列都为空
宏任务与微任务
JS 引擎把所有任务分成两类,一类叫宏任务(macroTask),一类叫微任务(microTask)
宏任务
- script(整体代码)
- setTimeout/setInterval
- I/O
- UI 渲染
- postMessage
- MessageChannel
- requestAnimationFrame
- setImmediate(Node.js 环境)
微任务
- new Promise().then()
- MutaionObserver
- process.nextTick(Node.js 环境)
经典题目 1
简单介绍后,接下来让我们来看几道经典题目吧:
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
Promise.resolve()
.then(function () {
console.log('promise1')
})
.then(function () {
console.log('promise2')
})
console.log('script end')
- 整体 script 作为第一个宏任务进入主线程,输出
script start
- 遇到 setTimeout,setTimeout 为宏任务,加入宏任务队列
- 遇到 Promise,其 then 回调函数加入到微任务队列;第二个 then 回调函数也加入到微任务队列
- 继续往下执行,输出
script end
- 检测微任务队列,输出
promise1
、promise2
- 进入下一轮循环,执行 setTimeout 中的代码,输出
setTimeout
最后执行结果为:
script start
script end
promise1
promise2
setTimeout
经典题目 2
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
- 整体 script 作为第一个宏任务进入主线程,代码自上而下执行,执行同步代码,输出
script start
- 遇到 setTimeout,加入到宏任务队列
- 执行 async1(),输出
async1 start
;然后遇到await async2()
,await 实际上是让出线程的标志,首先执行 async2(),输出async2
;把 async2() 后面的代码console.log('async1 end')
加入微任务队列中,跳出整个 async 函数。(async 和 await 本身就是 promise+generator 的语法糖。所以 await 后面的代码是微任务。) - 继续执行,遇到 new Promise,输出
promise1
,把.then()
之后的代码加入到微任务队列中 - 继续往下执行,输出
script end
。接着读取微任务队列,输出async1 end
,promise2
,执行完本轮的宏任务。继续执行下一轮宏任务的代码,输出setTimeout
最后执行结果为:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
可以知道,浏览器只保证 requestAnimationFrame 的回调在重绘之前执行,但没有确定的时间,何时重绘由浏览器决定。