JavaScript初级-JS异步,回调队列以及事件循环

        在我的JavaScript学习系列第一篇文章里面说过,调用堆栈一次可以执行一个函数,如果一个函数堵塞,整个浏览器都会直接冻结。而异步就是解决问题的方案。

        首先上代码:

setTimeout(callback, 1000);
function callback(){
    console.log('异步');
}

        setTimeout是浏览器API的一部分,是一种浏览器提供的工具,10s后,浏览器接收我们传入的回调函数并将其移动到回调队列(Callback Queue)中去。

        首先,setTimeout在全局上下文中运行,10s后计时器被触发,回调函数准备运行,但是它必须要先经过回调队列。

        回调队列是什么?

        回调队列就是一个队列数据结构,它是一个有序的函数队列,遵守先进先出(FIFO的原则。

        每个异步函数在被放入调用堆栈之前必须要通过回调队列,这时我们的第三个主角登场,事件循环(Event Loop)。

        事件循环的任务只有一个:检查调用堆栈是否为空,如果调用堆栈里是空闲的,而且回调队列里有某个函数,那么就将其放入调用堆栈中。

        这就是所谓的基本JS异步。


        再来深入的看一看,上代码:

console.log('1')
console.log('2')
setTimeout(() => {
  console.log('?')
}, 1000)
console.log('3')

        这段代码在输出之后会发现,"?"在3之后才被打印出来,也就是说,计时器在这没有堵塞住下面的代码,这个用上面的原理很容易解释:

        异步函数若想进入调用堆栈,必须先要通过回调队列,再经过事件循环的确定审查后再进入调用堆栈开始运行,真的挺惨一函数。

        再讨论一下我在之前文章里面说过的JavaScript线程的问题,JavaScript单线程是指浏览器中负责解释和执行JavaScript代码的只有一个线程,即JS引擎线程,但是浏览器的渲染进程是提供多个线程的,如下:

  • JS引擎线程
  • 事件触发线程
  • 定时触发器线程
  • 异步http请求线程
  • GUI渲染线程

        当JS引擎收到不同的请求时,它会用不同的线程去处理不同的请求,例如:DOM监听事件或者是网络请求,还有就像定时器等等吧,而JS引擎线程则继续后面的其他任务,这样就实现了异步非堵塞。

        但是像上面的定时触发线程,它是来处理定时器的,但是它的作用也仅仅是来定时而已,设置的时间一到,回调函数就会进入回调队列,等待Event Loop的采摘。

        类似的JS引擎线程遇到异步(DOM事件监听,网络请求,setTimeout计时器等...),会将这些异步任务交给相应的线程去维护,等待时机(用户点击DOM,网络请求成功,计时器结束),再之后就不必赘述了。

        对了,异步任务一般只有:网络请求,计时器和DOM事件监听

        其实我反复在讲的,其实都是一个事情,希望能看懂。


        接下来,我们更深入讨论。

        上代码:

console.log('script start')

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

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

console.log('script end')

// script start
// script end
// promise1
// promise2
// timer over

        这里我们会发现,"promise1"和"promise2"在"time over"之前就打印出来了,我们暂时不谈Promise,在这里我们不过多解释,等明天写一期关于Promise的。

        这里引入两个新的概念:宏任务(macro-task)和微任务(micro-task)。

        所有的任务都可以这么分类:

  • macro-task:主代码块、setTimeout、setInterval等(可以看到,回调队列中的每一个事件都是一个 macro-task,现在称之为宏任务队列)
  • micro-task:Promise、process.nextTick(暂时用不上)等

        JS引擎线程首先执行主代码块,在执行栈中每次执行的代码都是一个宏任务,回调堆栈的也是这样,但是在执行宏任务过程中遇到了Promise,就会创建微任务,加入到微任务队列队尾。

        微任务肯定是在宏任务执行的时候创建的,在这个宏任务的下一个宏任务执行开始之前,浏览器会对页面重新渲染。在上一个宏任务执行完成后,且在渲染页面之前,会执行当前微任务队列中的所有微任务。

        简单来说,在一个宏任务执行完之后,在重新渲染和开始下一个宏任务之前,将会把它执行期间内产生的所有微任务都执行完。

        这就可以解释上面的打印结果了,两个微任务是在"script end"这个宏任务执行时创建的,所以先执行两个微任务(两个微任务都是.then()创建的微任务,.then()会被分发到微任务中去)。

        注意:如果在微任务里面包含了微任务,那么先不管内层微任务,在处理掉外层后继续执行微任务队列,不给宏任务喘息的机会。

        而如果在微任务里面添加了宏任务呢,比如set1先加入了回调队列,而微任务里面的宏任务set2后加入回调队列,如果set2的最小延迟毫秒数小于set1,那么set2仍然先被执行,只比较这个第二参数毫秒。

        说到事件循环,第一轮事件循环是对整体代码的运行,整体可以看作是一个宏任务,宏任务放到回调队列,微任务放到微任务队列,一轮执行完后,检查一下是否有微任务,有的话就执行,然后重新渲染,再开始下一轮事件循环,对于不断嵌套的类型要一层一层解决,延迟毫秒数要重点注意

        最后就是一定要牢记:js是单线程执行,要有完整的事件循环思想。

        又看了几篇博客,看着内容都差不多,基本都概括到了,写麻了。

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

henuGM

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值