单线程是指在JS引擎中负责解释和执行JS代码的线程唯一,同一时间上只能执行一件任务。
1.为什么是单线程?
JS在浏览器中使用,浏览器渲染Dom,如果浏览器的JS引擎是多线程的,同时执行多段JS代码,而这些代码又操作了同一个DOM,则可能引起冲突。 比如process1删除了该DOM,process2又编辑了该DOM的样式,如果同时执行就会引起冲突。
2.为什么需要异步?
单线程意味着,所有任务都需要排队自上而下执行,后一个任务的执行必须等到前一个任务结束,如果不存在异步,那么一个任务执行耗时很长,后面的任务都会阻塞。
3.JS中的异步实现-事件流(Event Loop)
事件循环就是 主线程不断地从消息队列中取消息,执行消息。取一次,执行一次即完成了一次事件循环。 JS的执行机制就是判断任务是同步还是异步,同步就进入主线程,直接执行。异步就进入异步队列中,待同步函数执行完毕后,轮询执行异步队列中的回调函数。
例1:
console.log(1)
setTimeout(() => {
console.log(2)
})
console.log(3)
// 打印结果 1 → 3 → 2
按照刚才的执行机制去分析,setTimeout是异步任务,放到异步队列中,主线程自上而下,限制性打印1和3,然后执行异步队列的setTimeout函数,打印2。
这样分析确实没错,但不完全正确。
事实上,按照JS的执行机制,按照异步和同步划分方式,并不完全正确。
JS执行时,V8会创建一个全局执行上下文,在创建上下文的同时,V8也会在内部创建一个微任务队列,有微任务队列,自然也有宏任务队列。任务队列中的每一个任务都称为宏任务,在当前宏任务执行过程中,如果有新的微任务产生,就添加到微任务队列中,当前宏任务里的微任务全部执行完,才会执行下一个宏任务。
*微任务(micro-task)*包括: promise.then()、queueMicrotask()、MutationObserver(监听DOM)、node中的process.nextTick等。
*宏任务(macro-task)*包括:渲染事件、请求、script、setTimeout、setInterval、Node中的setImmediate、I/O等。
按照这个规则再解释上面的例1
<script> // 宏任务
console.log(1)
setTimeout(() => {
console.log(2)
}) // 宏任务
console.log(3)
</script>
因为setTimeout是宏任务,放入宏任务队列,需要等当前script宏任务执行完,才可以执行,所以打印结果是 1 → 3 → 2
再看一个经典例子,彻底搞清楚JS的Event Loop
<script>
setTimeout(() => {
console.log('setTimeout')
})
new Promise(resolve => {
console.log('promise1')
for (let i = 0; i < 1000; i++) {
(i === 999) & resolve()
}
console.log('promise2')
}).then(() => {
console.log('promise3')
})
console.log('script')
</script>
// 输出结果: promise1 → promise2 → script → promise3 → setTimeout
解释:
script宏任务 主线程执行script内容
setTimeout宏任务 放到宏任务队列等待
promise同步任务正常执行 打印promise1
for循环同步任务正常执行 打印promise2
promise.then() 微任务,放到微任务队列
继续向下执行,打印script
查看微任务队列,存在等待的微任务
执行微任务promise.then(), 打印 promise3
微任务队列空,去宏任务队列中查看,存在等待的宏任务
执行setTimeout,打印setTimeout