前端开发那些事儿

【前端进阶】深入浅出浏览器事件循环

2020-10-06  本文已影响0人  Gopal

引子:为什么会有事件循环

重点: javascript 从诞生之日起就是一门单线程的非阻塞的脚本语言

我们先来聊下 JavaScript 这两个特点:

如何做到非阻塞呢?这就需要我们的主角——事件循环(Event Loop

浏览器中的事件循环

我们看一个很经典的图,这张图基本可以概括了事件循环(该图来自演讲—— 菲利普·罗伯茨:到底什么是Event Loop呢? | 欧洲 JSConf 2014[1])后面演示用的 Loupe[2] 也是该演讲者写的((Loupe是一种可视化工具,可以帮助您了解JavaScript的调用堆栈/事件循环/回调队列如何相互影响))

[图片上传失败...(image-e56a0a-1601973516487)]

javascript 代码执行的时候会将不同的变量存于内存中的不同位置:堆(heap)和栈(stack)中来加以区分。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针

执行栈(call stack: 当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。 而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈

比如,如下是一段同步代码的执行

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">function a() { b(); console.log('a'); } function b() { console.log('b') } a(); </pre>

我们通过 Loupe 演示下代码的执行过程:

[图片上传失败...(image-3c4ea4-1601973516487)]

同步代码的执行过程是相对比较简单的,但涉及到异步执行的话,又是怎样的呢?

事件队列(callback queue): js 引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js 会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列

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

Loupe 官方的一个例子:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`$.on('button', 'click', function onClick() {
setTimeout(function timer() {
console.log('You clicked the button!');
}, 2000);
});

console.log("Hi!");

setTimeout(function timeout() {
console.log("Click the button!");
}, 5000);

console.log("Welcome to loupe.");` </pre>

[图片上传失败...(image-d78414-1601973516487)]

我们分析一下这个执行的过程:

再回头看看这张图,应该有种豁然开朗的感觉

[图片上传失败...(image-adf411-1601973516487)]

以上的过程按照类似如下的方式实现,queue.waitForMessage() 会同步地等待消息到达(如果当前没有任何消息等待被处理),故我们称之为事件循环(Event Loop

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">while (queue.waitForMessage()) { queue.processNextMessage(); } </pre>

微任务和宏任务

微任务——Micro-Task

常见的 micro-task:new Promise().then(callback)MutationObserve 等(asyncawait)实际上是 Promise 的语法糖

宏任务——Macro-Task

常见的 macro-tasksetTimeoutsetIntervalscript(整体代码)、 I/O 操作、UI 交互事件、postMessage

事件循环的执行顺序

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

Eveent Loop 的循环过程如下:

如下图所示:

[图片上传失败...(image-3801f0-1601973516486)]

执行顺序总结:执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环

[图片上传失败...(image-29d6f4-1601973516486)]

为了更好的理解,我们来看一个例子

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`console.log('start')

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

Promise.resolve().then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})

console.log('end')` </pre>

[图片上传失败...(image-533012-1601973516486)]

我们来分析一下:

故最后的结果如下:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">start end promise1 promise2 setTimeout </pre>

练习题

增加这个环境在于,现在面试笔试都会出事件循环的题目,实际上的可能比上面的例子难,原因在于微任务和宏任务涉及的知识点不少,这就需要我们进一步巩固我们的基础知识,我相信能够认真对待以下题目的,都能够更好的掌握事件循环

我就暂不做分析,大家不懂的有疑问的可以在评论区一起交流

题目一

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`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)
})` </pre>

<details data-tool="mdnice编辑器"><summary>点击查看答案</summary> start children4 children2 children3 children5 children7</details>

题目2

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`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');` </pre>

<details data-tool="mdnice编辑器"><summary>点击查看答案</summary> 3 end 2 4</details>

题目3

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">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') </pre>

<details data-tool="mdnice编辑器"><summary>点击查看答案</summary> script start async1 start async2 promise1 script end async1 end promise2 setTimeout</details>

题目4

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">let resolvePromise = new Promise(resolve => { let resolvedPromise = Promise.resolve() resolve(resolvedPromise); // 提示:resolve(resolvedPromise) 等同于: // Promise.resolve().then(() => resolvedPromise.then(resolve)); }) resolvePromise.then(() => { console.log('resolvePromise resolved') }) let resolvedPromiseThen = Promise.resolve().then(res => { console.log('promise1') }) resolvedPromiseThen .then(() => { console.log('promise2') }) .then(() => { console.log('promise3') }) </pre>

<details data-tool="mdnice编辑器"><summary>点击查看答案</summary> promise1 -> promise2 -> resolvePromise resolved -> promise3</details>

题目5

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`console.log('script start');

setTimeout(() => {
console.log('Gopal');
}, 1 * 2000);

Promise.resolve()
.then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});

async function foo() {
await bar()
console.log('async1 end')
}
foo()

async function errorFunc () {
try {
// Tips:参考:https://zh.javascript.info/promise-error-handling:隐式 try…catch
// Promise.reject()方法返回一个带有拒绝原因的Promise对象
// Promise.reject('error!!!') === new Error('error!!!')
await Promise.reject('error!!!')
} catch(e) {
console.log(e)
}
console.log('async1');
return Promise.resolve('async1 success')
}
errorFunc().then(res => console.log(res))

function bar() {
console.log('async2 end')
}

console.log('script end');` </pre>

<details data-tool="mdnice编辑器"><summary>点击查看答案</summary> script start async2 end script end promise1 async1 end error!!! async1 promise2 async1 success Gopal</details>

题目6

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">new Promise((resolve, reject) => { console.log(1) resolve() }) .then(() => { console.log(2) new Promise((resolve, reject) => { console.log(3) setTimeout(() => { reject(); }, 3 * 1000); resolve() }) .then(() => { console.log(4) new Promise((resolve, reject) => { console.log(5) resolve(); }) .then(() => { console.log(7) }) .then(() => { console.log(9) }) }) .then(() => { console.log(8) }) }) .then(() => { console.log(6) }) </pre>

<details data-tool="mdnice编辑器"><summary>点击查看答案</summary> 1 2 3 4 5 6 7 8 9</details>

题目7

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`console.log('1');

setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
})
new Promise((resolve) => {
console.log('4');
resolve();
}).then(() => {
console.log('5')
})
})

Promise.reject().then(() => {
console.log('13');
}, () => {
console.log('12');
})

new Promise((resolve) => {
console.log('7');
resolve();
}).then(() => {
console.log('8')
})

setTimeout(() => {
console.log('9');
Promise.resolve().then(() => {
console.log('10');
})
new Promise((resolve) => {
console.log('11');
resolve();
}).then(() => {
console.log('12')
})
})` </pre>

<details data-tool="mdnice编辑器"><summary>点击查看答案</summary> 1 7 12 8 2 4 9 11 3 5 10 12</details>

总结

本文从 JS 的两个特点:单线程以及非阻塞介绍了事件循环的必要性,因为事件循环在浏览器和 Node.js 的表现是很大不一样的,本人只谈论到了浏览器中的事件循环,并介绍了微任务和宏任务,以及它们的执行流程,最后通过 7 道题目帮助大家巩固知识

大家喜欢的话,别忘了点赞关注~

往期优秀文章推荐

参考

详解JavaScript中的Event Loop(事件循环)机制[9]

深入理解NodeJS事件循环机制[10]

并发模型与事件循环[11]

【前端体系】从一道面试题谈谈对EventLoop的理解[12]

菲利普·罗伯茨:到底什么是Event Loop呢? | 欧洲 JSConf 2014[13]

JavaScript中的Event Loop(事件循环)机制[14]

JS事件循环机制(event loop)之宏任务/微任务[15]

深入理解js事件循环机制(浏览器篇)[16]

从面试题看 JS 事件循环与 macro micro 任务队列[17]

参考资料

[1]

菲利普·罗伯茨:到底什么是Event Loop呢? | 欧洲 JSConf 2014: https://www.youtube.com/watch?v=8aGhZQkoFbQ [2]

Loupe: http://latentflip.com/loupe/?code=JC5vbignYnV0dG9uJywgJ2NsaWNrJywgZnVuY3Rpb24gb25DbGljaygpIHsKICAgIHNldFRpbWVvdXQoZnVuY3Rpb24gdGltZXIoKSB7CiAgICAgICAgY29uc29sZS5sb2coJ1lvdSBjbGlja2VkIHRoZSBidXR0b24hJyk7ICAgIAogICAgfSwgMjAwMCk7Cn0pOwoKY29uc29sZS5sb2coIkhpISIpOwoKc2V0VGltZW91dChmdW5jdGlvbiB0aW1lb3V0KCkgewogICAgY29uc29sZS5sb2coIkNsaWNrIHRoZSBidXR0b24hIik7Cn0sIDUwMDApOwoKY29uc29sZS5sb2coIldlbGNvbWUgdG8gbG91cGUuIik7!!!PGJ1dHRvbj5DbGljayBtZSE8L2J1dHRvbj4%3D [3]

一个合格的中级前端工程师应该掌握的 20 个 Vue 技巧: https://juejin.im/post/6872128694639394830 [4]

【Vue进阶】——如何实现组件属性透传?: https://juejin.im/post/6865451649817640968 [5]

前端应该知道的 HTTP 知识【金九银十必备】: https://juejin.im/post/6864119706500988935 [6]

最强大的 CSS 布局 —— Grid 布局: https://juejin.im/post/6854573220306255880 [7]

如何用 Typescript 写一个完整的 Vue 应用程序: https://juejin.im/post/6860703641037340686 [8]

前端应该知道的web调试工具——whistle: https://juejin.im/post/6861882596927504392 [9]

详解JavaScript中的Event Loop(事件循环)机制: https://zhuanlan.zhihu.com/p/33058983 [10]

深入理解NodeJS事件循环机制: https://juejin.im/post/6844903999506923528 [11]

并发模型与事件循环: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop [12]

【前端体系】从一道面试题谈谈对EventLoop的理解: https://juejin.im/post/6868849475008331783 [13]

菲利普·罗伯茨:到底什么是Event Loop呢? | 欧洲 JSConf 2014: https://www.youtube.com/watch?v=8aGhZQkoFbQ [14]

JavaScript中的Event Loop(事件循环)机制: https://segmentfault.com/a/1190000022805523 [15]

JS事件循环机制(event loop)之宏任务/微任务: https://juejin.im/post/6844903638238756878 [16]

深入理解js事件循环机制(浏览器篇): http://lynnelv.github.io/js-event-loop-browser [17]

从面试题看 JS 事件循环与 macro micro 任务队列: https://juejin.im/post/6844903796754104334

上一篇下一篇

猜你喜欢

热点阅读