浅谈javascript事件循环

2021-02-23  本文已影响0人  前端dog君

大家好,我是前端dog君,一名95后前端小兵。2019年毕业于北京化工大学,天津人,不知道有校友和老乡嘛?对前端的热爱,让我们在此相聚,希望这篇文章,能帮助到您,也同时希望能交到志同道合的小伙伴,共同发展,一起进步。我的微信号dm120225,备注简书,期待您的光临。

今天在公司遇到一个因javascript 事件循环引发的bug,趁着这个机会将js事件循环机制进行梳理。

js是单线程的

我们大家都知道,javascript语言的一大特点就是单线程,也就是说,同一时间只能做一件事,那么为什么javascript不能有多个线程呢?多线程可以提高响应速度,提高效率呀。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准呢?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

单线程的js如何执行异步代码?

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
js引擎执行异步代码而不用等待,是因有为有 消息队列和事件循环。

消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息。
事件循环:事件循环是指主线程重复从消息队列中取消息、执行的过程。

实际上,主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。

js事件循环

浏览器中的事件循环和node环境下事件循环大同小异,但是也会有细微的不同,这里我们暂时只介绍浏览器事件循环。

首先我们先介绍几个概念:

执行栈:同步代码的执行,按照顺序添加到执行栈中

事件队列:异步代码的执行,遇到异步事件不会等待它返回结果,而是将这个事件挂起,继续执行执行栈中的其他任务。当异步事件返回结果,将它放到事件队列中,被放入事件队列不会立刻执行起回调,而是等待当前执行栈中所有任务都执行完毕,主线程空闲状态,主线程会去查找事件队列中是否有任务,如果有,则取出排在第一位的事件,并把这个事件对应的回调放到执行栈中,然后执行其中的同步代码。

宏任务和微任务

JS 引擎把所有任务分成两类,一类叫宏任务(macroTask),一类叫微任务(microTask)。

为什么要引入微任务,只有一种类型的任务不行么?

页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么一种类型的任务就不合适了,所以引入了微任务队列。

我们来看下什么情况下回触发宏任务和微任务:

宏任务:

微任务:

运行机制

异步任务的返回结果会被放到一个任务队列中,根据异步事件的类型,这个事件实际上会被放到对应的宏任务和微任务队列中去。

在当前执行栈为空时,主线程会查看微任务队列是否有事件存在。

在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

面试题解析

我们找到几道关于javascript事件循环的经典面试题,一起来剖析一下

下面代码输出什么

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 start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

下面代码输出什么


console.log('start');
setTimeout(() => {
    console.log('children2');
    Promise.resolve().then(() => {
        console.log('children3');
    })
}, 0);

new Promise(function(resolve, reject) {
    console.log('children4');
    setTimeout(function() {
        console.log('children5');
        resolve('children6')
    }, 0)
}).then((res) => {
    console.log('children7');
    setTimeout(() => {
        console.log(res);
    }, 0)
})

这道题跟上面题目不同之处在于,执行代码会产生很多个宏任务,每个宏任务中又会产生微任务

最后的执行结果如下

start
children4
children2
children3
children5
children7
children6

下面代码输出什么

const p = function() {
    return new Promise((resolve, reject) => {
        const p1 = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(1)
            }, 0)
            resolve(2)
        })
        p1.then((res) => {
            console.log(res);
        })
        console.log(3);
        resolve(4);
    })
}


p().then((res) => {
    console.log(res);
})
console.log('end');

最后的执行结果如下

3
end
2
4

总结

js是一门单线程的语言,对异步代码的执行采取的是事件循环机制。我们无论在日常工作中还是面试中,可能会遇到一些奇奇怪怪的问题,这多半是由于javascript的事件循环机制引发的。我们可以回忆一下js事件循环过程,宏任务放入执行栈,微任务,宏任务,渲染,下一个宏任务放入执行栈,微任务,宏任务,渲染......相信大家了解了js的事件循环机制,在以后的工作中处理奇奇怪怪的bug,能够游刃有余。祝大家学习进步,天天开心,💪!

参考链接:
JS是单线程,你了解其运行机制吗?
JavaScript事件循环机制解析
深入理解 JavaScript 之事件循环(Event Loop)

我是前端dog君,一名95后前端小兵。对前端的热爱,让我们在此相聚,希望这篇文章,能帮助到您,也同时希望能交到志同道合的小伙伴,共同发展,一起进步。我的微信号dm120225,备注简书,期待您的光临。

上一篇 下一篇

猜你喜欢

热点阅读