JavaScript的单线程与异步

66 阅读3分钟

单线程是指在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