JS单线程与微任务宏任务

每一种语言都有自己的运行机制,javascript当然也不例外,了解了js的运行机制,对于想要深入学习js的我们来说有着莫大的帮助。好了话不多说,下面开始讲述一js的整个运行机制,但是在此之前你先要了解几个概念。下面一一列举,并配合实例来帮助大家理解记忆。

一、js是一门单线程的语言

那什么是单线程呢。?

单线程通俗的来说就是同一时间只能做一件事情,不能同时做几件事情,与传统的后台编程语言如java一般都是多线程的可以同时做几件事情,也就是所谓的并发。 说道为什么不把js设计为多线程并发的呢。?这样不是功能更加强大么。?这是因为在js的设计之初就是作为浏览器的脚本语言,主要作用就是与用户做交互,如果设计为多线程的话,那就会带来很多复杂的同步问题了,势必会影响浏览器的交互效果,不妨假设一下若js有两个线程,A线程需要将页面信息删除,B线程需要将页面信息修改显示,那就会让前端处理逻辑变得复杂了,也就会影响与用户的交互体验了。

所以从javascript诞生之初就被设计为单线程语言,估计这个特性在以后也不会被改变。

当然现在HTML5提出了Web Worker标准,允许javascript创建多线程,但是创建的子线程完全受主线程控制。而且子线程是不能访问DOM的,子线程的全局对象也不是window。这样当前端有一些巨大的数据计算的时候,可以充分利用多核CPU的计算能力。关于web worker后期我们会专门有一篇文章来讲述,在此不做过多说明。

(说到后端语言的并发插入一些额外的说明,与本文索要阐述的js单线程无关,以前还只是单核处理器的时候,其实大多是“伪并发”它是通过操作系统快速地在各个计算任务之间切换从而向用户展示一种“这些计算任务正在被并发执行”的假象,实际上,在某一确定的时间点上,只有一个线程被运行着。这是运行的速度足够的块,给用户感觉这些任务好像是同时进行的,到后来多核处理器普及了,由于系统中同时存在着多个处理器,那么我们就可以让多个线程在同一时刻同时运行在不同的处理器上。)

二、什么是任务队列

由于JS是一个单线程的语言,就需要一个专门来管理任务执行的队列,只有前一个任务执行完了,才会接着去执行下一个任务。但是有些任务耗费的时间很长,如果是cpu一直在忙碌在干事情,那倒也没什么,最怕的是此时cpu是处于空闲状态的。比如I/O操作,或者Ajax请求,有时候我们发出一个ajax到后端请求数据到浏览器接受到后端返回的数据,这段时间比较长。此时若是cpu一直处于空闲等待的状态的话,那岂不是会浪费很多性能。所以设计者就考虑不妨将这些需要等待的任务挂起,去执行后面的任务,等到所请求的数据返回或者I/O操作结束有了结果之后再回过头去执行刚刚被挂起的任务。

因此根据上述我们又可以将任务分为两类,一种是同步任务,一种是异步任务。我们将同步任务放在浏览器的主线程之上,只有前面一个任务执行完成之后才会执行后面的任务,异步任务是指 不进入主线程,而进入任务队列的任务。只有任务队列通知主线程,某个异步任务可以执行了,那么此时该任务才会进入主线程执行。

具体来说异步任务的执行机制如下:

  • 1:有一个主线程用于执行一些同步任务,形成一个执行栈。

  • 2:与此同时还有一个任务队列,当异步任务有了运行结果的时候,就会在任务队列中放置一个事件。

  • 3:一旦主线程上的同步任务都执行完了,系统就会依次读取任务队列上的任务。而此时正处于“等待”状态的异步任务也就结束等待了,开始进入执行栈中,开始执行。

  • 4:主线程不断反复的重复上面第三部。

一旦主线程上面的任务运行完之后就会从任务队列中读取新的任务继续运行。这就是javascript的运行机制,整个过程不断循环反复直至所有任务全部运行结束。

“任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列”,就是读取里面有哪些事件。

“任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列”,等待主线程读取。

三、回调函数

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。若是一个异步任务没有回调函数的话,那等异步任务返回数据后,执行栈执行什么呀。(⊙﹏⊙)

一般而言,任务队列是一个队列的数据结构(即先进先出),排在前面位置的任务会被主线程优先的读取执行。主线程从任务队列读取的过程基本上是自动的,即当主线程上任务执行完之后,就会自动从任务队列上读取任务去执行。

四、Event Loop

主线程从任务队列循环不断读取任务的这个过程我们称之为Event Loop循环即事件循环。

下面配一张图(图片来源于阮一峰老师的博客)来帮助大家更好的理解整个JS的运行机制
在这里插入图片描述
我们可以看到,这个V8引擎在解析的时候有一个任务栈,任务栈可以调用各种外部API来,而他们又可以不断的像任务队列中增加各种事件(click,load…)只要主线程运行完之后就会读取任务队列上的回调函数。

微任务 宏任务

首先,JavaScript是一个单线程的脚本语言。
  • 所以就是说在一行代码执行的过程中,必然不会存在同时执行的另一行代码,就像使用alert()以后进行疯狂console.log,如果没有关闭弹框,控制台是不会显示出一条log信息的。
    亦或者有些代码执行了大量计算,比方说在前端暴力破解密码之类的鬼操作,这就会导致后续代码一直在等待,页面处于假死状态,因为前边的代码并没有执行完。

  • 所以如果全部代码都是同步执行的,这会引发很严重的问题,比方说我们要从远端获取一些数据,难道要一直循环代码去判断是否拿到了返回结果么?就像去饭店点餐,肯定不能说点完了以后就去后厨催着人炒菜的,会被揍的。
    于是就有了异步事件的概念,注册一个回调函数,比如说发一个网络请求,我们告诉主程序等到接收到数据后通知我,然后我们就可以去做其他的事情了。
    然后在异步完成后,会通知到我们,但是此时可能程序正在做其他的事情,所以即使异步完成了也需要在一旁等待,等到程序空闲下来才有时间去看哪些异步已经完成了,可以去执行。
    比如说打了个车,如果司机先到了,但是你手头还有点儿事情要处理,这时司机是不可能自己先开着车走的,一定要等到你处理完事情上了车才能走。

微任务与宏任务的区别

这个就像去银行办业务一样,先要取号进行排号。
一般上边都会印着类似:“您的号码为XX,前边还有XX人。”之类的字样。

  • 因为柜员同时职能处理一个来办理业务的客户,这时每一个来办理业务的人就可以认为是银行柜员的一个宏任务来存在的,当柜员处理完当前客户的问题以后,选择接待下一位,广播报号,也就是下一个宏任务的开始。
    所以多个宏任务合在一起就可以认为说有一个任务队列在这,里边是当前银行中所有排号的客户。
    任务队列中的都是已经完成的异步操作,而不是说注册一个异步任务就会被放在这个任务队列中,就像在银行中排号,如果叫到你的时候你不在,那么你当前的号牌就作废了,柜员会选择直接跳过进行下一个客户的业务处理,等你回来以后还需要重新取号

  • 而且一个宏任务在执行的过程中,是可以添加一些微任务的,就像在柜台办理业务,你前边的一位老大爷可能在存款,在存款这个业务办理完以后,柜员会问老大爷还有没有其他需要办理的业务,这时老大爷想了一下:“最近P2P爆雷有点儿多,是不是要选择稳一些的理财呢”,然后告诉柜员说,要办一些理财的业务,这时候柜员肯定不能告诉老大爷说:“您再上后边取个号去,重新排队”。
    所以本来快轮到你来办理业务,会因为老大爷临时添加的“理财业务”而往后推。
    也许老大爷在办完理财以后还想 再办一个信用卡?或者 再买点儿纪念币?
    无论是什么需求,只要是柜员能够帮她办理的,都会在处理你的业务之前来做这些事情,这些都可以认为是微任务。

这就说明:你大爷永远是你大爷
在当前的微任务没有执行完成时,是不会执行下一个宏任务的。

所以就有了那个经常在面试题、各种博客中的代码片段:

setTimeout(_ => console.log(4))

new Promise(resolve => {
  resolve()
  console.log(1)
}).then(_ => {
  console.log(3)
})

console.log(2)
setTimeout就是作为宏任务来存在的,而Promise.then则是具有代表性的微任务,上述代码的执行顺序就是按照序号来输出的。

所有会进入的异步都是指的事件回调中的那部分代码
也就是说new Promise在实例化的过程中所执行的代码都是同步进行的,而then中注册的回调才是异步执行的。
在同步代码执行完成后才回去检查是否有异步任务完成,并执行对应的回调,而微任务又会在宏任务之前执行。

所以就得到了上述的输出结论1、2、3、4。

+部分表示同步执行的代码

setTimeout(_ => {
 console.log(4)
})

new Promise(resolve => {
  resolve()
  console.log(1)
}).then(_ => {
  console.log(3)
})
console.log(2)

本来setTimeout已经先设置了定时器(相当于取号),然后在当前进程中又添加了一些Promise的处理(临时添加业务)。

所以进阶的,即便我们继续在Promise中实例化Promise,其输出依然会早于setTimeout的宏任务:

setTimeout(_ => console.log(4))

new Promise(resolve => {
  resolve()
  console.log(1)
}).then(_ => {
  console.log(3)
  Promise.resolve().then(_ => {
    console.log('before timeout')
  }).then(_ => {
    Promise.resolve().then(_ => {
      console.log('also before timeout')
    })
  })
})

console.log(2)

当然了,实际情况下很少会有简单的这么调用Promise的,一般都会在里边有其他的异步操作,比如fetch、fs.readFile之类的操作。
而这些其实就相当于注册了一个宏任务,而非是微任务。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JavaScript 中,事件循环(Event Loop)是一个非常重要的概念。事件循环是一种机制,用于执行异步任务,以保证 JavaScript单线程的情况下能够处理多个任务。 在事件循环中,我们通常将任务分为宏任务(macro task)和微任务(micro task)两类。 宏任务是由浏览器或 Node.js 的 API 提供的异步任务,例如 setTimeout、setInterval、requestAnimationFrame、I/O 操作等。这些任务会被添加到任务队列(task queue)中,当主线程执行完当前任务后,会从队列中取出一个宏任务执行,直到队列为空。 微任务则是在当前任务执行结束后立即执行的异步任务,例如 Promise 的回调函数、MutationObserver 的回调函数等。这些任务会被添加到微任务队列(microtask queue)中,当主线程执行完当前任务后,会从微任务队列中按顺序取出所有任务执行,直到队列为空。 需要注意的是,微任务的执行优先级高于宏任务,也就是说,在执行宏任务过程中,如果有微任务需要执行,会先执行完所有微任务,然后再执行下一个宏任务。 下面是一个示例代码,用于演示宏任务微任务的执行顺序: ```javascript console.log('start'); setTimeout(() => { console.log('setTimeout'); }, 0); Promise.resolve().then(() => { console.log('Promise'); }); console.log('end'); ``` 上述代码中,先执行同步代码,输出 `start` 和 `end`,然后将 `setTimeout` 函数添加到宏任务队列中,并将 Promise 的回调函数添加到微任务队列中。最后,按顺序取出微任务队列中的任务,输出 `Promise`,再取出宏任务队列中的任务,输出 `setTimeout`。 输出结果如下: ``` start end Promise setTimeout ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值