Eventloop 学习总结

产生背景

javascript从诞生之日起就是一门单线程的非阻塞的脚本语言。这是由其最初的用途来决定的:与浏览器交互。

单线程意味着,javascript代码在执行的任何时候,都只有一个主线程来处理所有的任务。

而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。

单线程是必要的,也是javascript这门语言的基石,原因之一在其最初也是最主要的执行环境——浏览器中,我们需要进行各种各样的dom操作。试想一下 如果javascript是多线程的,那么当两个线程同时对dom进行一项操作,例如一个向其添加事件,而另一个删除了这个dom,此时该如何处理呢?因此,为了保证不会 发生类似于这个例子中的情景,javascript选择只用一个主线程来执行代码,这样就保证了程序执行的一致性。

前面提到javascript的另一个特点是“非阻塞”,那么javascript引擎到底是如何实现的这一点呢?答案就是今天这篇文章的主角——event loop(事件循环)。

由于浏览器和nodejs在实现eventloop时机制有不同之处,所以将分开讲述这两种实现方法

浏览器的Eventloop

Eventloop事件循环中的执行栈事件队列宏任务微任务等概念需要首先理清

什么是执行栈

执行栈

执行栈就是js代码运行的地方,上图call stack所示。

什么是事件队列

当浏览器中的事件监听函数被触发(DOM)、网络请求的相应(ajax)、定时器被触发(setTimeout)相对应的回调函数就会被推送到事件队列中,等待执行;如上图中的Callback Queue。

什么是宏队列

script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering

什么是微任务

process.nextTick(Nodejs), Promises, Object.observe, MutationObserver;

事件循环(event-loop)是什么?

事件循环Eventloop

宏队列与微任务的执行顺序宏队列与微任务的执行顺序

具体来说,浏览器会不断从task队列中按顺序取task执行,每执行完一个task都会检查microtask队列是否为空(执行完一个task的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去task队列中取下一个task执行,以此类推。

例子说明一切

在这里插入图片描述

首先,全局代码(main())压入调用栈执行,打印start

接下来setTimeout压入macrotask队列,promise.then回调放入microtask队列,最后执行console.log(‘end’),打印出end

至此,调用栈中的代码被执行完成,回顾macrotask的定义,我们知道全局代码属于macrotask,macrotask执行完,那接下来就是执行microtask队列的任务了,执行promise回调打印promise1

promise回调函数默认返回undefined,promise状态变为fullfill触发接下来的then回调,继续压入microtask队列,event loop会把当前的microtask队列一直执行完,此时执行第二个promise.then回调打印出promise2

这时microtask队列已经为空,从上面的流程图可以知道,接下来主线程会去做一些UI渲染工作(不一定会做),然后开始下一轮event loop,执行setTimeout的回调,打印出setTimeout

这个过程会不断重复,也就是所谓的事件循环

async/await

async 做一件什么事情?

带 async 关键字的函数,它使得你的函数的返回值必定是 promise 对象

也就是

如果async关键字函数返回的不是promise,会自动用Promise.resolve()包装

如果async关键字函数显式地返回promise,那就以你返回的promise为准

await 在等什么?

await等的是右侧「表达式」的结果

也就是说,

右侧如果是函数,那么函数的return值就是「表达式的结果」

右侧如果是一个 ‘hello’ 或者什么值,那表达式的结果就是 ‘hello’

async function async1() {
    console.log( 'async1 start' )
    await async2()
    console.log( 'async1 end' )
}
async function async2() {
    console.log( 'async2' )
}
async1()
console.log( 'script start' )

这里注意一点,可能大家都知道await会让出线程,阻塞后面的代码,那么上面例子中, ‘async2’ 和 ‘script start’ 谁先打印呢?

是从左向右执行,一旦碰到await直接跳出, 阻塞async2()的执行?

还是从右向左,先执行async2后,发现有await关键字,于是让出线程,阻塞代码呢?

实践的结论是,从右向左的。先打印async2,后打印的script start

await 等到之后,做了一件什么事情?

那么右侧表达式的结果,就是await要等的东西。

等到之后,对于await来说,分2个情况

  • 不是promise对象
  • 是promise对象

如果不是 promise , await会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,作为 await表达式的结果

如果它等到的是一个 promise 对象,await 也会暂停async后面的代码,先执行async外面的同步代码,等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果。

注意:

在async/await的处理上谷歌70和73实现是不同,具体可以看这篇文章

Node JS 的Eventloop实现

在这里插入图片描述

  • timers 阶段:这个阶段执行timer(setTimeoutsetInterval)的回调
  • I/O callbacks 阶段:执行一些系统调用错误,比如网络通信的错误回调
  • idle, prepare 阶段:仅node内部使用
  • poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
  • check 阶段:执行 setImmediate() 的回调
  • close callbacks 阶段:执行 socketclose 事件回调

timers 阶段

timers 是事件循环的第一个阶段,Node 会去检查有无已过期的timer,如果有则把它的回调压入timer的任务队列中等待执行,事实上,Node 并不能保证timer在预设时间到了就会立即执行,因为Node对timer的过期检查不一定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲。比如下面的代码,setTimeout()setImmediate() 的执行顺序是不确定的。

setTimeout(() => {
    console.log('setTimeout');
}, 0);
setImmediate(() => {
    console.log('setImmediate');
})

执行的结果是这样子的:

setTimeout和setImmediate比较

首先进入的是timers阶段,如果我们的机器性能一般,那么进入timers阶段,一毫秒已经过去了(setTimeout(fn, 0)等价于setTimeout(fn, 1)),那么setTimeout的回调会首先执行。

如果没有到一毫秒,那么在timers阶段的时候,下限时间没到,setTimeout回调不执行,事件循环来到了poll阶段,这个时候队列为空,此时有代码被setImmediate(),于是先执行了setImmediate()的回调函数,之后在下一个事件循环再执行setTimemout的回调函数。

而我们在执行代码的时候,进入timers的时间延迟其实是随机的,并不是确定的,所以会出现两个函数执行顺序随机的情况。

再对比一段代码:

var fs = require('fs')

fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0);
    setImmediate(() => {
        console.log('immediate');
    });
});

这里我们就会发现,setImmediate永远先于setTimeout执行。

原因如下:

fs.readFile的回调是在poll阶段执行的,当其回调执行完毕之后,poll队列为空,而setTimeout入了timers的队列,此时有代码被setImmediate(),于是事件循环先进入check阶段执行回调,之后在下一个事件循环再在timers阶段中执行有效回调。

poll 阶段

poll 阶段主要有2个功能:

  • 处理 poll 队列的事件
  • 当有已超时的 timer,执行它的回调函数

even loop将同步执行poll队列里的回调,直到队列为空或执行的回调达到系统上限(上限具体多少未详),接下来even loop会去检查有无预设的setImmediate(),分两种情况:

  1. 若有预设的setImmediate(), event loop将结束poll阶段进入check阶段,并执行check阶段的任务队列
  2. 若没有预设的setImmediate(),event loop将阻塞在该阶段等待

注意一个细节,没有setImmediate()会导致event loop阻塞在poll阶段,这样之前设置的timer岂不是执行不了了?所以咧,在poll阶段event loop会有一个检查机制,检查timer队列是否为空,如果timer队列非空,event loop就开始下一轮事件循环,即重新进入到timer阶段。

check 阶段

setImmediate()的回调会被加入check队列中, 从event loop的阶段图可以知道,check阶段的执行顺序在poll阶段之后。

Nodejs的Eventloop小结

  • event loop 的每个阶段都有一个任务队列
  • 当 event loop 到达某个阶段时,将执行该阶段的任务队列,直到队列清空或执行的回调达到系统上限后,才会转入下一个阶段
  • 当所有阶段被顺序执行一次后,称 event loop 完成了一个 tick

一个例子

在这里插入图片描述

首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2

至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask队列的所有任务,依次打印promise1promise2

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值