本文
前往“校招VIP”小程序,访问更方便

【校招VIP】JavaScript事件循环机制

csdn 08月23日

转载声明:文章来源:https://blog.csdn.net/qq_40992225/article/details/126063875

JS是一门单线程语言,单线程就意味着,所有的任务需要排队,前一个任务结束,才会执行下一个任务。这样所导致的问题是:如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。为了解决这个问题,JS中出现了同步和异步。他们的本质区别是:一条流水线上各个流程的执行顺序不同。

事件循环Event Loop

事件循环(eventLoop)是单线程的JavaScript在处理异步事件时进行的一种循环过程,具体来讲,对于异步事件它会先加入到事件队列中挂起,等主线程空闲时会去执行事件队列中的事件。

通过这个概念,我们需要了解5个小概念:

主线程:所有的同步任务都是在主线程里执行的,异步任务可能会在macrotask或者microtask里面

同步任务: 在主线程上排队执行的任务,只有一个任务执行完毕,才能执行下一个任务,

异步任务: 不进入主线程,而是放在任务队列中,若有多个异步任务则需要在任务队列中排队等待,任务队列类似于缓冲区,任务下一步会被移到执行栈然后主线程执行调用栈的任务。

宏任务:(macro)task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

常见的宏任务:script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI render

微任务:microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。

常见的微任务:new Promise().then(回调)、async/await、process.nextTick(node)、mutationObserver(html5新特性)

执行栈与任务队列

1)执行栈:从名字可以看出,执行栈使用到的是数据结构中的栈结构, 它是一个存储函数调用的栈结构,遵循先进后出的原则。它主要负责跟踪所有要执行的代码。 每当一个函数执行完成时,就会从堆栈中弹出(pop)该执行完成函数;如果有代码需要进去执行的话,就进行 push 操作。

JavaScript在按顺序执行执行栈中的方法时,每次执行一个方法,都会为它生成独有的执行环境(上下文),当这个方法执行完成后,就会销毁当前的执行环境,并从栈中弹出此方法,然后继续执行下一个方法。

2)任务队列: 从名字中可以看出,任务队列使用到的是数据结构中的队列结构,它用来保存异步任务,遵循先进先出的原则。它主要负责将新的任务发送到队列中进行处理。

事件循环Event Loop执行机制

主线程任务——>微任务——>宏任务 如果宏任务里还有微任务就继续执行宏任务里的微任务,如果宏任务中的微任务中还有宏任务就在依次进行

主线程任务——>微任务——>宏任务——>宏任务里的微任务——>宏任务里的微任务中的宏任务——>直到任务全部完成 我的理解是在同级下,微任务要优先于宏任务执行

同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。

当指定的事情完成时,Event Table会将这个函数移入Event Queue。

主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。

上述过程会不断重复,也就是常说的Event Loop(事件循环)

下面是有关事件循环的例题:

第一题:

<script>
setTimeout(() => {
console.log(1)
}, 20)
console.log(2);
setTimeout(() => {
console.log(3)
}, 10)
console.log(4)
// console.time ("AA")
for (let i = 0; i < 90000000; i++) {
// do soming
}
// console. timeEnd("AA") //=>AA: 33ms 左右
console.log(5)
setTimeout(() => {
console.log(6)
}, 20)
console.log(7)
setTimeout(() => {
console.log(8)
}, 10)
</script>

最终的输出结果为:

其中:

// console.time ("AA")
for (let i = 0; i < 90000000; i++) {
// do soming
}
// console. timeEnd("AA") //=>AA: 33ms 左右

这块代码并不是没有用,它是有用意的,因为它占用了运行时间,所以上面两个timeout的挂载时间点后后面的两个不一样,这个代码片段也是同步代码,它将时间点进行了延迟。
也就是在0ms时打印输出了2,4,然后通过了那个代码片段用时33ms左右,接着33ms以后循环执行完毕接着打印输出下面的5,7,此时同步代码结束。接着执行异步任务:(10ms以后放入第2个宏任务,20ms之后放入第一个宏任务,43ms以后放入第4个宏任务,53ms放入第3个宏任务)但是同步执行完了已经是33ms以后了,所以异步任务的前两个就要立即执行,所以紧接着打印输出3,1,等43ms以后打印输出8,53ms以后打印输出6.

第二题:

<script>
async function async1() {
console.log('A');
await async2()
console.log('B');
}
async function async2() {
console.log('C');
}
console.log('D');

setTimeout(function() {
console.log('E');
}, 0)
async1()

new Promise(function(resolve) {
console.log('F');
resolve()
}).then(function() {
console.log('G');
})
console.log('H');
</script>

此时输出的结果是:

按照上述代码的执行顺序,第一步打印输出D,这一点并无异议,接着是setTimeout()宏任务,暂不执行,置于队列中;接着是async1()函数的执行,打印输出A;紧接着是await async2(),await就是一个变相的Promise,Promise.then不就是一个变相的微任务嘛,此时这一段代码就相当于

new Promise((resolve)=>{
console.log('C');
resolve()
}).then(res=>{
console.log('B');
})

所以我们先打印了C,把后面的then里面的内容放到异步操作里面。

紧接着执行一个Promise,同理打印输出F,then后面的放到异步操作的微任务里面,

接着打印输出H。

此时同步操作执行完毕,再去查看异步操作的任务。这个时候就只剩下两个then以及一个setTimeout了,而Promise.then方法是常见的微任务,要比setTimeout这个宏任务先执行,所以先打印输出B,接着G,最后是E。

第三题

<body>
<button id="button">button</button>
<script>
const button = document.getElementById("button")

button.addEventListener("click", () => {
Promise.resolve().then(() => console.log("Microtask 1"))
console.log("Listener 1")
})
button.addEventListener("click", () => {
Promise.resolve().then(() => console.log("Microtask 2"))
console.log("Listener 2")
})
// press button
// button.click()
</script>
</body>

现在有一个按钮,我们将这个按钮绑定点击事件,现在问题来了,如果我打开了网页,点击了这个按钮,结果会是什么?

但是,如果我们直接用button.click(),即模拟浏览器点击了一下按钮,此时的结果又是什么呢?会发生变化吗?

原因是

如果是手动点击按钮的话,其实是触发了两个事件,也就是上面两段代码之间互相是没有关联的,我们先执行最上面一个点击事件,里面先执行它的同步任务,再执行微任务,也就是先输出Listener 1,接着Microtask 1,然后它这个任务就完整结束掉了,接着执行下面一个任务,打印输出Listener 2,接着Microtask 2

而模拟浏览器的点击事件就相当于我在这个任务中有且只有一个宏任务,你可以理解这段代码就是一段主线程,也就是调用栈里面是一个点击事件,然后这个点击事件先执行微任务,再是宏任务,所以打印输出的结果为上面截图所示。

暂无回复