JavaScript的事件循环\运行机制\eventloop

吴志春

共 18835字,需浏览 38分钟

 ·

2021-07-31 03:22

JavaScript的运行机制是工作中最常碰到的,同时也是笔试 or 面试中会被问到的问题,故在本文整理了JavaScript的运行机制,以供参考。


JavaScript执行机制,重点有两点:

1.JavaScript是一门单线程语言。

2.Event Loop(事件循环)是JavaScript的执行机制。


既然说js是单线程,那就是在执行代码的时候是从上往下执行的,先来看一段代码:


 1setTimeout(function(){
2  console.log('定时器开始')
3});
4
5new Promise(function(resolve){
6   console.log('Promise开始');
7   resolve();
8}).then(function(){
9  console.log('执行then函数')
10});
11
12console.log('代码执行结束');


输出结果:

Promise开始

代码执行结束

执行then函数

定时器开始


关于javascript

javascript是一门单线程语言,在最新的HTML5中提出了Web-Worker, 但javascript是单线程这一核心扔未改变。所以一切javascript版的“多线程”都是用单线程模拟出来的,一切javascript多线程都是纸老虎


JS为什么是单线程的

最初设计JS是用来在浏览器验证表单操控DOM元素的是一门脚本语言,如果js是多线程的,那么两个线程同时对一个DOM元素进行了相互冲突的操作,那么浏览器的解析器是无法执行的。


js为什么需要异步

如果js中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞。对于用户而言,阻塞就意味着“卡死”,这样就导致了很差的用户体验。比如在进行ajax请求的时候如果没有返回数据后面的代码就没办法执行


js单线程又是如何实现异步的呢

js中的异步以及多线程都可以理解成为一种“假象”,就拿h5的WebWorker来说,子线程有诸多限制,不能控制DOM,不能修改全局对象等等,通常只用来做计算做数据处理。
这些限制并没有违背我们之前的观点,所以说是“假象”。JS异步的执行机制其实就是事件循环(eventloop),理解了eventloop机制,就理解了js异步的执行机制。


JS的事件循环(eventloop)是怎么运作的

事件循环、eventloop\运行机制 这三个术语其实说的是同一个东西
“先执行同步操作异步操作排在事件队列里”这样的理解其实也没有任何问题但如果深入的话会引出很多其他概念,比如event table和event queue, 我们来看运行过程:

  1. 首先判断JS是同步还是异步,同步就进入主线程运行,异步就进入event table.

  2. 异步任务在event table中注册事件,当满足触发条件后,(触发条件可能是延时也可能是ajax回调),被推入event queue

  1. 同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主线程中。

cf51e2ced8cacc86c53640b930263fe4.webp

那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查 主线程 执行栈是否为空,一旦为空,就会去event queue那里检查是否有等待被调用的函数。


1let data = [];
2$.ajax({
3    url:www.javascript.com,
4    data:data,
5    success:() => {
6        console.log('发送成功!');
7    }
8})
9console.log('代码执行结束');
  • ajax进入event table,注册回调函数success

  • 执行 console.log('代码执行结束')

  • ajax事件完成,回调函数success进入event queue

  • 主线程从event queue读取回调函数success并执行


setTimeout

1setTimeout(() => {
2  console.log('2秒到了')
3}, 2000)


setTimeout是异步操作首先进入event table, 注册的事件就是它的回调,触发条件就是2秒之后,当满足条件回调被推入event queue,当主线程空闲时会去event queue里查看是否有可执行的任务。


1console.log(1// 同步任务进入主线程
2setTimeout(fun(),0)   // 异步任务,被放入event table, 0秒之后被推入event queue里
3console.log(3// 同步任务进入主线程


1、3是同步任务马上会被执行,执行完成之后主线程空闲去event queue(事件队列)里查看是否有任务在等待执行,这就是为什么setTimeout的延迟事件是0毫秒却在最后执行的原因

但setTimeout延时的时间有时候并不是那么准确


1setTimeout(() => {
2  console.log('2秒到了')
3}, 2000)
4wait(9999999999)


分析运行过程:

  1. console进入Event Table并注册,计时开始。

  2. 执行sleep函数,sleep方法虽然是同步任务但sleep方法进行了大量的逻辑运算,耗时超过了2秒

  1. 2秒到了,计时事件timeout完成,console进入Event queue, 但是sleep还没执行完,主线程还被占用,只能等着。

  2. sleep终于执行完了, console终于从event queue进入了主线程执行,这个时候已经远远超过了2秒

其实延迟2秒只是表示2秒后,setTimeout里的函数被推入event queue , 而event queue(事件队列)里的任务,只有在主线程空闲时才会执行。


上述流程走完,我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务(本例子中为console)加入到event queue中, 又因为单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于2秒。我们还经常遇到setTimeout(fn,0)这样的代码,它的含义是,指定某个任务在主线最早的空闲时间执行,意思就是不用再等多少秒了, 只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。但是即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。


setIntval

以setIntval(fn,ms)为例,setIntval是循环执行的,setIntval会每隔指定的时间将注册的函数置入event queue,不是每过ms会执行一次fn,而是每过ms秒,会有fn进入event queue。需要注意一点的是,一旦setIntval的回调函数fn执行时间超过了延迟事件ms,那么就完成看不出来有时间间隔了。


除了广义的同步任务和异步任务,我们对任务有更精细的定义:


  • 宏任务 包含整个script代码块,setTimeout, setIntval

  • 微任务 Promise , process.nextTick


在划分宏任务、微任务的时候并没有提到async/ await的本质就是Promise


那事件循环机制到底是怎么样的?

不同类型的任务会进入对应的event queue, 比如setTime和setIntval会进入相同(宏任务)的event queue, 而Promise和process.nextTick会进入相同(微任务)的event queue.



Promise与事件循环

Promise在初始化时,传入的函数是同步执行的,然后注册then回调。注册完之后,继续往下执行同步代码,在这之前,then的回调不会执行。同步代码块执行完毕后,才会在事件循环中检测是否有可用的promise回调,如果有,那么执行,如果没有,继续下一个事件循环。

1. 宏任务,微任务都是队列, 一段代码执行时,会先执行宏任务中的同步代码。2. 进行第一轮事件循环的时候会把全部的js脚本当成一个宏任务来运行。

3. 如果执行中遇到setTimeout之类的宏任务,那么就把这个setTimeout内部的函数推入[宏任务的队列]中,下一轮宏任务执行时调用。

4. 如果执行中遇到promise.then()之类的微任务,就会推入到[当前宏任务的微任务队列]中, 在本轮宏任务的同步代码都执行完成后,依次执行所有的微任务。

5. 第一轮事件循环中当执行完全部的同步脚步以及微任务队列中的事件,这一轮事件循环就结束了, 开始第二轮事件循环。

6. 第二轮事件循环同理先执行同步脚本,遇到其他宏任务代码块继续追加到[宏任务的队列]中,遇到微任务,就会推入到[当前宏任务的微任务队列]中,在本轮宏任务的同步代码执行都完成后, 依次执行当前所有的微任务。

7. 开始第三轮循环往复..


下面用代码来深入理解上面的机制:


 1setTimeout(function() {
2    console.log('4')
3})
4
5new Promise(function(resolve{
6    console.log('1'// 同步任务
7    resolve()
8}).then(function() {
9    console.log('3')
10})
11console.log('2')



  1. 这段代码作为宏任务,进入主线程。

  2. 先遇到setTimeout,那么将其回调函数注册后分发到宏任务event queue.

  3. 接下来遇到Promise, new Promise立即执行,then函数分发到微任务event queue

  4. 遇到console.log(), 立即执行

  5. 整体代码script作为第一个宏任务执行结束, 查看当前有没有可执行的微任务,执行then的回调。(第一轮事件循环结束了,我们开始第二轮循环)

  6. 从宏任务的event queue开始,我们发现了宏任务event queue中setTimeout对应的回调函数,立即执行。执行结果:1-2-3-4


  7. 整体script作为第一个宏任务进入主线程,遇到console.log(1)输出1

  8. 遇到setTimeout, 其回调函数被分发到宏任务event queue中。我们暂且记为setTimeout1
    3.遇到process.nextTick(),其回调函数被分发到微任务event queue中,我们记为process1
    4.遇到Promise, new Promise直接执行,输出7.then被分发到微任务event queue中,我们记为then1

  9. 又遇到setTimeout,其回调函数被分发到宏任务event queue中,我们记为setTimeout2.

  10. 现在开始执行微任务, 我们发现了process1和then1两个微任务,执行process1,输出6,执行then1,输出8, 第一轮事件循环正式结束, 这一轮的结果输出1,7,6,8.那么第二轮事件循环从setTimeout1宏任务开始

  11. 首先输出2, 接下来遇到了process.nextTick(),统一被分发到微任务event queue,记为process2
    8.new Promise立即执行,输出4,then也被分发到微任务event queue中,记为then2

  12. 现在开始执行微任务,我们发现有process2和then2两个微任务可以执行输出3,5. 第二轮事件循环结束,第二轮输出2,4,3,5. 第三轮事件循环从setTimeout2宏任务开始
    10。直接输出9,跟第二轮事件循环类似,输出9,11,10,12

  13. 完整输出是1,7,6,8,2,4,3,5,9,11,10,12(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)


 1console.log('1')
2setTimeout(function() {
3    console.log('2')
4    process.nextTick(function() {
5        console.log('3')
6    })
7    new Promise(function(resolve{
8        console.log('4')
9        resolve()
10    }).then(function() {
11        console.log('5')
12    })
13})
14
15process.nextTick(function() {
16    console.log('6')
17})
18
19new Promise(function(resolve{
20    console.log('7')
21    resolve()
22}).then(function() {
23    console.log('8')
24})
25
26setTimeout(function() {
27    console.log('9')
28    process.nextTick(function() {
29        console.log('10')
30    })
31    new Promise(function(resolve{
32        console.log('11')
33        resolve()
34    }).then(function() {
35        console.log('12')
36    })
37})


如果是setTimeout里面嵌套setTimeout, 那么嵌套的setTimeout的宏任务要在外面的宏任务排序的后面,往后排。看个例子


 1new Promise(function (resolve
2    console.log('1')// 宏任务一
3    resolve()
4}).then(function () {
5    console.log('3'// 宏任务一的微任务
6})
7setTimeout(function () // 宏任务二
8    console.log('4')
9    setTimeout(function () // 宏任务五
10        console.log('7')
11        new Promise(function (resolve{
12            console.log('8')
13            resolve()
14        }).then(function () {
15            console.log('10')
16            setTimeout(function () {  // 宏任务七
17                console.log('12')
18            })
19        })
20        console.log('9')
21    })
22})
23setTimeout(function () // 宏任务三
24    console.log('5')
25})
26setTimeout(function () {  // 宏任务四
27    console.log('6')
28    setTimeout(function () // 宏任务六
29        console.log('11')
30    })
31})
32console.log('2'// 宏任务一


初步总结:宏任务是一个栈按先入先执行的原则,微任务也是一个栈也是先入先执行。但是每个宏任务都对应会有一个微任务栈,宏任务在执行过程中会先执行同步代码再执行微任务栈。


async/await是什么

我们创建了promise但不能同步等待它执行完成。我们只能通过then传一个回调函数这样很容易再次陷入promise的回调地狱。实际上, async/await在底层转换成了promise和then回调函数,也就是说, 这是promise的语法糖。每次我们使用await, 解释器都创建一个promise对象,然后把剩下的async函数中的操作放到then回调函数中。async/await的实现,离不开promise. 从字面意思来理解, async是“异步”的简写,而await是async wait的简写可以认为是等待异步方法执行完成。


async/await用来干什么

用来优化promise的回调问题,被称为是异步的终极解决方案


async/await内部做了什么

async函数会返回一个Promise对象,如果在函数中return一个直接量(普通变量),async会把这个直接量通过Promise.resolve()封装成Promise对象。如果你返回了promise那就以你返回的promise为准。await是在等待,等待运行的结果也就是返回值。await后面通常是一个异步操作(promise),但是这不代表await后面只能跟异步才做,await后面实际是可以接普通函数调用或者直接量。
async相当于 new Promise,await相当于then


await的等待机制

如果await后面跟的不是一个promise,那await后面表达式的运算结果就是它等到的东西,如果await后面跟的是一个promise对象,await它会'阻塞'后面的diamante,等着promise对象resolve, 然后得到resolve的值作为await表达式的运算结果。但是此"阻塞"非彼“阻塞”,这就是await必须用在async函数中的原因。async函数调用不会造成"阻塞",它内部所有的“阻塞”都被封装在一个promise对象中异步执行(这里的阻塞理解成异步等待更合理)


async/await在使用过程中有什么规定

每个async方法都返回一个promise, await只能出现在async函数中


async/await在什么场景使用

单一的promise链并不能发现async/await的优势,但是如果需要处理由多个promise组成的then链的时候,优势就能体现出来了(Promise通过then链来解决多层回调的问题,现在又用async/awai来进一步优化它)


async/await如何使用

假设一个业务,分多个步骤完成,每个步骤都是异步的且依赖于上一个步骤的结果


 1function myPromise(n{
2    return new Promise(resolve => {
3        console.log(n)
4        setTimeout(() => resolve(n+1), n)
5    })
6}
7function step1(n{
8    return myPromise(n)
9}
10function step2(n{
11    return myPromise(n)
12}
13function step3(n{
14    return myPromise(n)
15}
16
17//如果用 Promise 实现
18step1(1000)
19.then(a => step2(a))
20.then(b => step3(b))
21.then(result => {
22    console.log(result)
23})
24
25//如果用 async/await 来实现呢
26async function myResult() {
27    const a = await step1(1000)
28    const b = await step2(a)
29    const result = await step3(b)
30    return result
31}
32myResult().then(result => {
33    console.log(result)
34}).catch(err => {
35    // 如果myResult内部有语法错误会触发catch方法
36})


看的出来async/await的写法更多优雅一些要比promise的链接调用更多直观也易于维护

我们来看在任务队列中async/await的运行机制,先给出大概方向再通过案例来证明:

  1. async定义的是一个promise函数和普通函数一样只要不调用就不会进入事件队列。

  2. async内部如果没有主动return promise, 那么async会把函数的返回值用promise包装

  3. await关键字必须出现在async函数中,await后面不是必须要跟一个异步操作,也可以是一个普通表达式

  4. 遇到await关键字,await右边的语句会被立即执行然后await下面的代码进入等待状态,等待await得到结果。await后面如果不是promise对象,await会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,作为await表达式的结果。await后面如果是promise对象,await也会暂停async后面的代码,先执行async外面的同步代码,等着promise对象fulfilled,然后把resolve的参数作为await表达式的运算结果。


 1setTimeout(function () {
2  console.log('6')
3}, 0)
4console.log('1')
5async function async1() {
6  console.log('2')
7  await async2()
8  console.log('5')
9}
10async function async2() {
11  console.log('3')
12}
13async1()
14console.log('4')
  1. 6是宏任务在下一轮事件循环执行

  2. 先同步输出1,然后调用async1(),输出2

  1. await async2()会先运行async2(), 5进入等待状态

  2. 输出3, 这个时候先执行async函数外的同步代码输出4.

  1. 最后await拿到等待的结果继续往下执行输出5.
    6, 进入第二轮事件循环输出6.
    12bf13e70f6b7d06f72f0f6ff1b75f0e.webp


测试代码的输出结果,看到async1函数输出2, 立马执行await 的async2函数,输出3, 但是没有立即返回,而是先执行async2外面的同步代码,最后得到返回

值111111给await async2函数,


3c805d5d01a04a56d22fea15f625c049.webp


 1console.log('1')
2async function async1() {
3  console.log('2')
4  await 'await的结果'
5  console.log('5')
6}
7
8async1()
9console.log('3')
10
11new Promise(function (resolve{
12  console.log('4')
13  resolve()
14}).then(function () {
15  console.log('6')
16})


  1. 首先输出1, 然后进入async1函数,输出2

  2. await后面虽然是一个直接量,但是还是先执行async函数外的同步代码

  1. 输出3, 进入promise输出4,then回调进入微任务队列

  2. 现在同步代码执行完了, 回到async函数继续执行输出5
    5, 最后运行微任务输出6


 1async function async1() {
2  console.log('2')
3  await async2()
4  console.log('7')
5}
6
7async function async2() {
8  console.log('3')
9}
10
11setTimeout(function () {
12  console.log('8')
13}, 0)
14
15console.log('1')
16async1()
17
18new Promise(function (resolve{
19  console.log('4')
20  resolve()
21}).then(function () {
22  console.log('6')
23})
24console.log('5')


  1. 首先输出同步代码1, 然后进入async1方法输出2

  2. 因为遇到await所以先进群async2方法, 后面的7处于等待状态

  1. 在async2中输出3, 现在跳出async函数先执行外面的同步代码

  2. 输出4,5.then回调进入微任务栈

  1. 现在宏任务执行完了,然后回到async1函数接着往下执行输出7

  2. 执行微任务输出6

  1. 进入下一轮事件循环输出8
    完整输出:1-2-3-4-5-7-6-8


 1async function async1() {
2  console.log('2')
3  const data = await async2()
4  console.log(data)
5  console.log('8')
6}
7
8async function async2() {
9  return new Promise(function (resolve{
10    console.log('3')
11    resolve('await的结果')
12  }).then(function (data{
13    console.log('6')
14    return data
15  })
16}
17console.log('1')
18
19setTimeout(function () {
20  console.log('9')
21}, 0)
22
23async1()
24
25new Promise(function (resolve{
26  console.log('4')
27  resolve()
28}).then(function () {
29  console.log('7')
30})
31console.log('5')
  1. 函数async1和async2只是定义先不管它, 首先输出1

  2. setTimeout作为宏任务进入宏任务队列等待下一轮事件循环

  1. 进入async1函数输出2,await下面的代码进入等待状态。

  2. 进入async2函数输出3,then回调进入微任务队列

  1. 现在执行外面的同步代码, 输出4,5,then回调进入微任务队列
    6, 按序执行微任务,输出6,7. 现在回到async1函数,
    7, 输出data, 也就是await关键字等到的内容, 接着输出8
    8, 进行下一轮事件循环输出9
    执行结果:1-2-3-4-5-6-7-await的结果-8-9


 1setTimeout(function () {
2  console.log('8')
3}, 0)
4
5async function async1() {
6  console.log('1')
7  const data = await async2()
8  console.log('6')
9  return data
10}
11
12async function async2() {
13  return new Promise(resolve => {
14    console.log('2')
15    resolve('async2的结果')
16  }).then(data => {
17    console.log('4')
18    return data
19  })
20}
21
22async1().then(data => {
23  console.log('7')
24  console.log(data)
25})
26
27new Promise(function (resolve{
28  console.log('3')
29  resolve()
30}).then(function () {
31  console.log('5')
32})
  1. setTimeout作为宏任务进入宏任务队列等待下一轮事件循环

  2. 先执行async1函数, 输出1,然后6进入等待状态,现在执行async2

  1. 输出2, then回调进入微任务队列
    4, 接下来执行外面的同步代码3, then回调进入微任务队列
    5, 按序列执行微任务,输出4,5. 下面会带async1函数
    6,输出了4之后执行return data, await拿到了内容
    7, 继续执行输出6, 执行了后面的return data才出发async1的then回调输出7以及data
    8, 进行第二轮事件循环输出8,
    完整输出结果:1-2-3-4-5-6-7-async2的结果-8



面试题题目一:


 1setTimeout(function() {
2    console.log('4')
3})
4
5new Promise(function(resolve{
6    console.log('1'// 同步任务
7    resolve()
8}).then(function() {
9    console.log('3')
10})
11console.log('2')


输出结果:1-2-3-4


 1console.log('1')
2setTimeout(function() {
3    console.log('2')
4    process.nextTick(function() {
5        console.log('3')
6    })
7    new Promise(function(resolve{
8        console.log('4')
9        resolve()
10    }).then(function() {
11        console.log('5')
12    })
13})
14
15process.nextTick(function() {
16    console.log('6')
17})
18
19new Promise(function(resolve{
20    console.log('7')
21    resolve()
22}).then(function() {
23    console.log('8')
24})
25
26setTimeout(function() {
27    console.log('9')
28    process.nextTick(function() {
29        console.log('10')
30    })
31    new Promise(function(resolve{
32        console.log('11')
33        resolve()
34    }).then(function() {
35        console.log('12')
36    })


输出结果:
1-7-6-8-2-4-3-5-9-11-10-12


 1setTimeout(function () {
2  console.log('6')
3}, 0)
4console.log('1')
5async function async1() {
6  console.log('2')
7  await async2()
8  console.log('5')
9}
10async function async2() {
11  console.log('3')
12}
13async1()
14console.log('4')


输出结果:
1-2-3-4-5-6


 1console.log('1')
2async function async1() {
3  console.log('2')
4  await 'await的结果'
5  console.log('5')
6}
7
8async1()
9console.log('3')
10
11new Promise(function (resolve{
12  console.log('4')
13  resolve()
14}).then(function () {
15  console.log('6')
16})


输出结果:1-2-3-4-5-6



最后听一首悦耳的歌放松放松,回忆学到的东西。

点击下面16ac0b1d26877fec41139325f50c09c1.webp播放音乐


长按二维码关注,一起努力。

助力寻人启事

微信公众号回复 加群 一起学习。

浏览 21
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报