浏览器基础(3)-浏览器中的 Event Loop

JavaScript到底是什么?

JavaScript是一个单线程、非阻塞、异步、解释性脚本语言。单线程的运行环境,使的它有且只有一个调用栈,它每次自能做一件事。

JavaScript调用(执行)栈

可以把调用栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。

在这里插入图片描述
上面这个图的大致过程是这样的,首先我们会有一个main函数,它指代文件本身。printSquare函数被调用了,我们将其压入栈,在printSquare函数中又调用了一个函数squared函数,将其压入栈。在squared函数中我么们又调用了multiply函数,一样的将其压入栈。现在该出栈了,multiply函数返回16后出栈,squared函数同样返回16出栈,printSquare函数在打印输出后也出栈,main函数出栈。

我们调试代码中也可以看到调用(执行)栈的身影
window.onload = () =>{
    function foo() {
        throw new Error('Error');
    }

    function bar() {
        foo();
    }

    function baz() {
        bar();
    }

    baz();
}

在这里插入图片描述
我们可以看到当执行foo函数时报错,接着在bar中报错,再到baz中报错。

你也许听过内存泄露,OK!。让我们看一个例子。
window.onload = () =>{
    function foo() {
        return foo();
    }
    foo();
}

在这里插入图片描述
我们可以看到报的错是表示栈溢出。

阻塞

让我们来看一个例子
在这里插入图片描述
我们给点击函数绑定一个下面的函数

function loop() {
    Promise.resolve().then(loop)
}
loop();

点击按钮过后会阻塞页面渲染。因为JavaScript是单线程的,所以会阻塞页面渲染,我们应该如何处理?最简单的就是提供异步回调。于是就有了异步事件的概念,注册一个回调函数,比如说发一个网络请求,我们告诉主程序等到接收到数据后通知我,然后我们就可以去做其他的事情了。
然后在异步完成后,会通知到我们,但是此时可能程序正在做其他的事情,所以即使异步完成了也需要在一旁等待,等到程序空闲下来才有时间去看哪些异步已经完成了,可以去执行。

浏览器中的 Event Loop

上一小节我们讲到了什么是调用栈,大家也知道了当我们执行 JS 代码的时候其实就是往执行栈中放入函数,那么遇到异步代码的时候该怎么办?其实当遇到异步的代码时,会被挂起并在需要执行的时候加入到 Task(有多种 Task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。

微任务与宏任务的区别

这个就像去银行办业务一样,先要取号进行排号。
一般上边都会印着类似:“您的号码为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之类的操作。

宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering。
微任务包括 process.nextTick ,promise ,MutationObserver。

Event loop

其中task为宏任务,microtask为微任务。
在这里插入图片描述
步骤
1. 首先我们从Task队列中取出一个任务按照其函数的调用顺序放入调用栈。
2. 先执行该任务的同步代码,如果遇到异步代码且是微任务就放入Microtask队列中。
3. 接下来执行该任务的所有微任务
4. 最后执行该任务的所有宏任务中的异步代码。
5. 开始下一次Event loop直到Task队列为空。

这里很多人会有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话才会先执行微任务。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JS的事件循环是指JS引擎在执行任务时的一种机制。在浏览器,事件循环是由浏览器来实现的,而在NodeJS也有自己的事件循环实现。事件循环的基本原理是将待处理的任务按顺序存放在一个任务队列,然后从队列取出任务并执行。在事件循环,任务可以分为宏任务和微任务两种类型。宏任务包括整体的script代码、setTimeout、setInterval等,而微任务则包括Promise、MutationObserver等。在事件循环的执行过程,微任务的执行优先于宏任务。 具体来说,事件循环的流程如下: 1. 执行当前的同步任务,即执行JS代码的同步代码。 2. 检查是否存在微任务,如果存在,则按照先进先出的顺序依次执行微任务,直到微任务队列为空。 3. 当前的宏任务执行完成后,检查是否存在新的宏任务。如果存在,则执行下一个宏任务,否则继续等待新的任务加入队列。 4. 重复步骤2和步骤3,直到任务队列为空。 在NodeJS,除了浏览器的事件循环机制外,还有一些差异和新增的任务类型和任务阶段。具体来说,NodeJS的事件循环包括以下几个阶段: 1. timers阶段:执行定时器回调函数。 2. pending callbacks阶段:执行延迟到下一个循环迭代的I/O回调函数。 3. idle, prepare阶段:仅在内部使用。 4. poll阶段:检索新的I/O事件;执行I/O相关的回调函数。 5. check阶段:执行setImmediate()的回调函数。 6. close callbacks阶段:执行关闭的回调函数,如socket.on('close', ...)。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值