前言🌞
- 一天 我在敲着代码
- 突然经理说加个需求让UI和后端给我扔了原型图和接口 我说可以但是先把手头的码完
- 好 等我码完了去看原型图 这时候又加了需求又有原型图和接口
我以上的执行顺序应该是怎么样的呢?
本文最后将通过下面这个贴近生活的案例分析js事件循环机制,如果不想重复查看理论知识可以直接移步到文章最后喔~
为什么 Javascript 要是单线程的 ❓
- 我们都知道,
javascript
从诞生之日起就是一门单线程的非阻塞的脚本语言。这是由其最初的用途来决定的:与浏览器交互,也就是说javascript
在处理任务的时候,所有任务只能在一个线程上排队被执行,试想一下如果javascript
是多线程的,当我们用javascript
操作DOM
的时候,一个线程来控制增加另一个来控制删除,这时浏览器应该听哪个线程的,如何判断优先级? - 为了避免这样的问题,
Javascript
在最初就选择了单线程执行。
执行栈与事件队列🍜
执行栈
- 当一个js文件执行的时候,会产生一个对应的执行环境也叫执行上下文。
- 每调用一个函数会产生一个新的执行上下文。
- 而当一系列函数被依次调用的时候,因为
javascript
是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方,这个地方被称为执行栈。 - 全局执行上下文会先放入栈中,然后执行一个函数时会把这个上下文放进栈中,然后进入函数中进行执行,遇到新的函数执行会把新的执行上下文再次放入栈中,然后执行函数里面的代码,知道代码执行完毕,会把对应执行上下文(出栈)销毁,等待垃圾回收。
比如这个例子:
console.log('我是第一个'); function caseFn() { console.log('我在case里面的上下文'); sthFn() console.log('sth执行完才到我'); } function sthFn(){ console.log('我在sth里面的上下文'); } caseFn(); console.log('最后才到我'); //输出:我是第一个 我在case里面的上下文 我在sth里面的上下文 sth执行完才到我 最后才到我
事件队列
- 如果说
Javascript
是单线程的,那么如果某一个任务耗时特别长怎么办呢?总不能让他一直等待到执行完才执行下一步吧? - 上文提到
Javascript
的另一大特点是非阻塞,js
引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。 - 当一个异步事件返回结果后,
Javascript
会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。 - 被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码。
比如这个例子:
console.log('我是第一个'); function caseFn() { console.log('我在case里面的上下文'); } setTimeout(function() { console.log('最后是我啊!') }, 0) caseFn(); console.log('最后才到我?'); //输出:我是第一个 我在case里面的上下文 最后才到我? 最后是我啊!
这执行栈与事件队列配合的整个过程叫做事件循环,但实际上会有这么简单吗?
宏任务与微任务👻
- 至此之前我认为的事件循环一直以为只有执行栈和事件队列两层,当遇到异步就放到队列最后执行,但是实际上还有第三层。
- 也就是说实际上因为异步任务之间并不相同,因此他们的执行优先级也有区别。不同的异步任务被分为两类:宏任务(
macro task
)和微任务(micro task
)。 - 在最新标准中,它们被分别称为
task
与jobs
。 - macro-task(宏任务) 大概包括:
setTimeout
,setInterval
,setImmediate(NodeJs)
,I/O
,UI rendering
。 - micro-task(微任务) 大概包括:
process.nextTick(NodeJs)
,Promise
,Object.observe
(已废弃),MutationObserver
(html5新特性)
事件循环机制(EventLoop)🚀
- 在上文中我们讲过,在一个事件循环中,先执行同一个上下文中同步的代码,遇到异步事件返回结果后会被放到一个
事件队列
中。 - 然而这个事件队列里面也是有优先级的,根据这个异步事件的类型,这个事件实际上会被对应的
宏任务队列
或者微任务队列
中去。 - 这个
微任务队列
是存在于放入执行队列
之后,事件队列
之前的。 - 首先,事件循环整体代码
script
入栈开始。 - 当执行栈执行完毕他会先查看微任务队列有没有可执行的事件,如果没有则去宏任务队列中取出一个事件并把对应的回到加入当前执行栈。
- 如果有,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈。
- 如此重复就是
js
事件的循环,也就是说当当前执行栈执行完毕没有东西可以跑了-->
他就去找微任务-->
再去找事件队列队列中的宏任务-->
然后在宏任务中如果有异步事件则根据类别又放在不同的宏任务队列和微任务队列中--->
反复循环直到没有事件可以执行。 - 只要记住在同一次事件循环中,微任务永远在宏任务之前执行,即先清空微任务
举个栗子🌰
- 想必如果第一次看这个概念的小伙伴肯定看不明白(我以前也是这样的(手动狗头)),没关系接下来用几个栗子来简单操作下。
简单的例子:
console.log('1'); setTimeout(function() { console.log('2') }, 1000) new Promise(resolve => { console.log('3') resolve(5); }).then(t => { console.log(t) }); console.log('4');
- 首先我们进入执行栈,遇到同步代码打印
1
- 遇到异步事件
setTimeout
的回调要放到任务队列里面的宏任务队列。
此时宏任务队列:setTimeout事件,微任务队列:无事件
- 遇到异步事件
Promise
,Promise
构造函数中的第一个参数,是在new
的时候执行,构造函数执行时,里面的参数进入执行栈执行;而后续的.then
则会被分发到微任务队列中去。所以会先打印3
,然后执行resolve
,将then
的回调分配到对应微任务队列。
此时宏任务队列:setTimeout
事件,微任务队列:then
的回调
- 遇到同步代码打印
4
- 第一次循环的执行栈没有代码就去找微任务队列,发现有个
then
的回调,所以打印5
- 当所有的微任务执行完毕之后,表示第一轮的循环就结束了
- 开始第二轮的循环,找到宏任务队列有个
setTimeout
,打印2
- 最后的结果是
1 3 4 5 2
什么?没听明白 那我换种说法👀
console.log('完成功能1') //这时经理突然提了需求让UI和后端分别给了设计图(Promise)和接口(setTimeout) setTimeout(function() { console.log('对接接口代码') }, 1000) new Promise(resolve => { console.log('查看设计图但没写代码') resolve('根据设计图写布局代码'); }).then(t => { console.log(t) }); console.log('完成功能2')
- 有一天兴高采烈写着‘
BUG
’,很顺利一开始就完成了功能1(打印完成功能1
) - 经理突然走过来说:'来来来,刚定了个需求我让UI和后端给你原型图和接口,你找时间完成它'
- 后端先给我了个
接口文档
(这里指setTimeout
),我心想我设计图都没拿到呢看接口浪费时间呀我先把工作完成,然后就等着稍后再看接口文档 - 这时候UI给了我
设计图
(这里指Promise
),我心想,万一要修改的页面和我现在在做的页面冲突就不好了,我先看一眼待会再切图(resolve
),这时打印(查看设计图但没写代码
) - 看完设计图发现跟现在的没啥关系嘛然后就继续完成之前的需求(
完成功能2
) - 好啦现在之前的需求都做完了,那我先看看新需求吧,找出设计图开始切图(执行
then
的回调,打印根据设计图写布局代码
) - 切完图拉就要去对接接口了,找回之前后端发的文档开始对接(
对接接口代码
) - 结果很快很顺利完成了所有需求,经理很开心又奖励了一个需求....
以上比喻可能不那么恰当,但是应该可以帮助理解一下这个机制
什么?你觉得简单了,那好吧我再举一个例子👂
难一点点的例子:
console.log('完成功能1') //这时经理突然提了需求页面1让UI和后端分别给了原型图(Promise)和接口(setTimeout) setTimeout(function() { //接口文档1 console.log('对接接口文档1') new Promise(function(resolve) { // 设计图2 console.log('查看设计图2但不切图') resolve() }).then(function() { console.log('根据设计图2完成布局') }) }, 0) new Promise(function(resolve) { //设计图1 console.log('查看设计图1但不切图') resolve() }).then(function() { console.log('根据设计图1完成布局') //经理又提了需求页面2让后端给了接口文档2 setTimeout(function() { // 接口文档2 console.log('对接接口文档2') }) }) console.log('完成功能2')
- 有一天兴高采烈写着‘
BUG
’,很顺利一开始就完成了功能1(打印完成功能1
) - 经理突然走过来说:'来来来,刚定了个需求我让UI和后端给你原型图和接口,你找时间完成它'
- 后端先给我了个
接口文档1
(这里指第一个setTimeout
),我心想我设计图都没拿到呢看接口浪费时间呀我先把工作完成,然后就等着稍后再看接口文档1 - 这时候UI给了我设计图(这里指遇到的第一个
Promise
),我心想,万一要修改的页面和我现在在做的页面冲突就不好了,我先看一眼待会再切图(resolve
),这时打印(查看设计图1但不切图
) - 看完设计图发现跟现在的没啥关系嘛然后就继续完成之前的需求(打印
完成功能2
) - 好啦现在之前的需求都做完了,那我先看看新需求吧,找出
设计图1
开始切图(执行then
的回调,打印根据设计图1完成布局
) - 好家伙,正当我为页面1的布局苦恼时,经理走过来说:'看你做挺快呀,再给你个功能帮忙做一下',就让后端人员先把
接口文档2
(这里指遇到的第二个setTimeout
)给了过来 - 我心想我页面1还没做完呢,待会再看
接口文档2
,我找回接口文档1
开始对接(打印对接接口文档1
) - 正当我快对接完成页面1接口时,UI把
设计图2
(这里指遇到的第二个Promise
)给了我,我先看一眼(打印查看设计图2但不切图
) - 这时候我完成了一开始的需求和新增的第一个需求,第二个需求的设计图和接口我都有了,我就根据
设计图2
开始切图(打印根据设计图2完成布局
) - 最后再找到
接口文档2
开始对接(打印对接接口文档2
)
以上就是举得一个小例子,可能不太恰当,还请大佬们鞭策
写在最后👋
- 我们的
JavaScript
的执行过程是单线程的,所有的任务可以看做存放在两个队列中——执行队列
和事件队列
。 执行队列
里面是所有同步代码的任务,事件队列
里面是所有异步代码的宏任务
,而我们的微任务
,是处在两个队列之间。当JavaScript
执行时,优先执行完所有同步代码
,遇到对应的异步代码,就会根据其任务类型存到对应队列(宏任务
放入事件队列
,微任务
放入执行队列之后,事件队列之前
);当执行完同步代码之后,就会执行位于执行队列和事件队列之间的微任务
,然后再执行事件队列中的宏任务
,这就是JS
事件循环机制。- 如果您觉得这篇文章有帮助到您的的话不妨点赞支持一下哟~~😛
- 这是我在学习之后,根据自己的想法理解所写出来的,难免会存在错误,各位大佬可以鞭策!
参考
详解JavaScript中的Event Loop(事件循环)机制