再谈 Event Loop,这下别说你不会

594 阅读10分钟

参考文章
最后一次搞懂 Event Loop
从一道面试题谈谈对EventLoop的理解
setTimeout+Promise+Async输出顺序?很简单呀!

要想引出EventLoop,我们先从一句话开始

1.JS的运行机制

有人说:“JS是一门单线程的非阻塞脚本语言”。

1.1 JS为什么是单线程语言,那它是怎么实现异步编程(非阻塞)运行的?

JS为什么是单线程语言???
JS怎么实现异步编程(非阻塞)运行???

先在脑海里记住上面两个问题,我们往下走~

2.进程和线程

进程:进程是 CPU 资源分配的最小单位(是能拥有资源和独立运行的最小单位)

线程:线程是 CPU 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位)

image

线程依赖进程,一个进程可以有一个或者多个线程,但是线程只能是属于一个进程。

2.1 JS的单线程

js的单线程指的是javaScript引擎只有一个线程。

那为什么只有一个线程???

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

这就决定了它只能是单线程的,否则会带来很复杂的同步问题。

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。为了防止主线程的阻塞,JavaScript 有了 同步异步 的概念。

2.2 同步和异步

  • 同步:你给朋友打电话咨询一件事,朋友说他查查,你不挂电话在等待,朋友把查到的结果告诉你,这期间你不能做自己的事情
// 下面这段段代码首先会弹出 alert 框,如果你不点击 确定 按钮,所有的页面交互都被锁死,并且后续的 console 语句不会被打印出来。

alert('Yancey');
console.log('is');
console.log('the');
console.log('best');
  • 异步:你给朋友打电话咨询一件事,朋友说他查查,回头告诉你,你把电话挂了,先去做自己的事情

2.3 继续学习几个概念~

image

先来解释上图中出现的几个单词所要表达的含义。
==Heap(堆)、Stack(栈)、Queue(队列)、Event Loop(事件轮询)==

2.3 程序中的堆栈队列

Heap 堆

堆, 是一种动态存储结构,是利用完全二叉树维护的一组数据,堆分为两种,一种为最大堆,一种为最小堆,将根节点最大的堆叫做最大堆大根堆,根节点最小的堆叫做最小堆小根堆。 堆是线性数据结构,相当于一维数组,有唯一后继。

Stack 栈

栈在程序中的设定是限定仅在表尾进行插入或删除操作的线性表。 栈是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。栈是只能在某一端插入和删除的特殊线性表。 image

Queue 队列

队列特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。 进行插入操作的端称为队尾,进行删除操作的端称为队头。 队列中没有元素时,称为空队列
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出 image

2.4 JS中的堆栈队列

堆, 动态分配的内存,大小不定也不会自动释放,存放引用类型,指那些可能由多个值构成的对象,保存在堆内存中,包含引用类型的变量,实际上保存的不是变量本身,而是指向该对象的指针。可以简单理解为存储代码块。

堆的作用:存储引用类型值的数据

let obj = {
    name: 'Even'remark: '登峰造极'
}

let func = () => {
    console.log('hello world')
}

栈(执行栈)

js中的栈准确来将应该叫调用栈(ECStack),会自动分配内存空间,会自动释放,存放基本类型,简单的数据段,占据固定大小的空间。

栈的作用:存储基本类型值,还有一个很要的作用。提供代码执行的环境

队列(任务队列,事件队列)

js中的队列可以叫做任务队列异步队列,任务队列里存放各种异步操作所注册的回调,里面分为两种任务类型,宏任务(macroTask)和微任务(microTask)。

到现在我们知道事件的执行需要执行环境,也就是执行栈;还知道同步任务和异步任务,还有存放异步事件的地方,叫做事件队列。那他们之间必定按照一套规则去执行,才能做到既是单线程,又绝对不会阻塞的核心机制。

这套规则就是我们今天要重点介绍的———— Event Loop(事件轮询)

终于引出了Event Loop(事件轮询)~

3. Event Loop

事件轮询就是解决javaScript单线程对于异步操作的一些缺陷,让 javaScript做到既是单线程,又绝对不会阻塞的核心机制,是用来协调各种事件、用户交互、脚本执行、UI 渲染、网络请求等的一种机制。

event-loop-processing-model 规范定义了 Event Loop 的循环过程:

3.1 Event Loop 的循环过程

一个Event Loop只要存在,就会不断执行下边的步骤:

  1. 在tasks(任务)队列中选择最老的一个task,用户代理可以选择任何task队列,如果没有可选的任务,则跳到下边的microtasks步骤。
  2. 将上边选择的task设置为正在运行的task。
  3. Run: 运行被选择的task。
  4. 将Event Loop的currently running task变为null。
  5. 从task队列里移除前边运行的task。
  6. Microtasks: 执行microtasks任务检查点。(也就是执行microtasks队列里的任务)
  7. 更新渲染(Update the rendering):可以简单理解为浏览器渲染...
  8. 如果这是一个worker event loop,但是没有任务在task队列中,并且WorkerGlobalScope对象的closing标识为true,则销毁Eveent Loop,中止这些步骤,然后进行定义在Web workers章节的run a worker。 9.返回到第一步。

==这么多字 相当于不讲人话 我们不看也罢!!== 我们直接看大佬们总结概括之后的结论。

概括来说:

  • Eveent Loop会不断循环的去取tasks队列的中最老的一个task(可以理解为宏任务)推入栈中执行,并在当次循环里依次执行并清空microtask队列里的任务。
  • 执行完microtask队列里的任务,有可能会渲染更新。(浏览器很聪明,在一帧以内的多次dom变动浏览器不会立即响应,而是会积攒变动以最高60HZ(大约16.7ms每帧)的频率更新视图)
1.遇到同步代码直接执行
2.遇到异步代码先放一边,并且将他回调函数存起来,存的地方叫事件队列
3.等所有同步代码都执行完,再从事件队列中把存起来的所有异步回调函数拿出来按顺序执行

上代码!

console.log(1) // 同步
setTimeout(() => {
  console.log(2) // 异步
}, 2000);
console.log(3) // 同步
setTimeout(() => {
  console.log(4) // 异步
}, 0);
console.log(5) // 同步

image

输出:1 3 5 4 2

前面说了,等所有同步代码都执行完,再从事件队列里依次执行所有异步回调函数。

其实事件队列也是一个小团体,人家也有自己的规则。为什么事件队列里需要有自己的规则呢?要不你先想想为什么学校里的社团里要有自己的规则要分等级,是因为有的人能力强有的人能力弱,所以也就有了等级的高低。其实事件队列也一样,事件队列是用来存异步回调的,但是异步也分类型啊,异步任务分为宏任务微任务,并且==微任务执行时机先于宏任务==

3.2 宏任务和微任务

3.2.1 我们先看下宏任务和微任务分别都有哪些?

宏任务 (macrotask)

  • script(整体代码)
  • setTimeout/setInterval
  • setImmediate(Node环境)
  • requestAnimationFrame

微任务 (microtask)

  • Promise的then()、catch()、finally()里面的回调
  • process.nextTick (Node 环境)

3.2.2 执行流程

image

3.3 光说不练假把式!

习题一

console.log(1)
setTimeout(() => { 
  console.log(2)
  Promise.resolve().then(() => {
    console.log(3)
  })
});
console.log(4)
new Promise((resolve,reject) => { 
  console.log(5)
  resolve()
}).then(() => {
  console.log(6)
  setTimeout(() => {
    console.log(7)
  })
})
console.log(8)

解析:

console.log(1) // 同步
setTimeout(() => { // 异步:宏任务 setTimeout1
  console.log(2) 
  Promise.resolve().then(() => { // 异步:微任务 then2
    console.log(3)
  })
});
console.log(4) // 同步
new Promise((resolve,reject) => {
  console.log(5) // 同步
  resolve()
}).then(() => { // 异步:微任务 then1
  console.log(6)
  setTimeout(() => { // 异步:宏任务 setTimeout2
    console.log(7) 
  })
})
console.log(8) // 同步
  1. 执行外层同步任务:1 4 5 8;
  2. 执行到 setTimeout 放到宏任务队列,此时宏任务队列 setTimeout1;
  3. 执行到 then 放到微任务队列,微任务队列 then 1;
  4. 执行 then1 :6 ,碰到宏任务 setTimeout2,此时宏任务队列 setTimeout1,setTimeout2;
  5. 执行 setTimeout1 :2 ,碰到微任务 then2 ,此时宏任务队列 setTimeout2,微任务队列 then2;
  6. 执行 then2 :3;
  7. 执行 setTimeout2 : 7;

所以最终输出: 1 4 5 8 6 2 3 7

再来一道测试一下!

习题二

setTimeout(() => {
  console.log('A');
}, 0);
var obj = {
  func: function() {
    setTimeout(function() {
      console.log('B');
    }, 0);
    return new Promise(function(resolve) {
      console.log('C');
      resolve();
    });
  },
};
obj.func().then(function() {
  console.log('D');
});
console.log('E');
  1. 第一个 setTimeout 放到宏任务队列,此时宏任务队列为 ['A']
  2. 接着执行 obj 的 func 方法,将 setTimeout 放到宏任务队列,此时宏任务队列为 ['A', 'B']
  3. 函数返回一个 Promise,因为这是一个同步操作,所以先打印出 'C'
  4. 接着将 then 放到微任务队列,此时微任务队列为 ['D']
  5. 接着执行同步任务 console.log('E');,打印出 'E'
  6. 因为微任务优先执行,所以先输出 'D'
  7. 最后依次输出 'A' 和 'B'

所以最终输出:C E D A B

习题三

当遭遇 Async/Await 时又会变成什么样?
async function async1() {
  console.log(1);
  await async2();
  console.log(2);
}
async function async2() {
  console.log(3);
}
console.log(4);
setTimeout(function () {
  console.log(5);
});
async1()
new Promise(function (resolve, reject) {
  console.log(6);
  resolve();
}).then(function () {
  console.log(7);
});
console.log(8);

转化为

console.log(4); // 同步
setTimeout(function () {
  console.log(5); // 异步:宏任务 setTimeout
});

// async1函数可转换成
console.log(1) // 同步
new Promise((resolve, reject) => {
  console.log(3) // 同步
  resolve()
}).then(() => { // 异步:微任务 then1
  console.log(2)
})
// async1函数结束

new Promise(function (resolve, reject) {
  console.log(6); // 同步
  resolve();
}).then(function () { // 异步:微任务 then2
  console.log(7);
});
console.log(8); // 同步

这样再去按照之前的解题思路走一遍

正确答案 4 1 3 6 8 2 7 5

最后一题

async function async1() {
    console.log('A')
    await async2()
    console.log('B')
}

async function async2() {
    console.log('C')
    return new Promise((resolve, reject) => {
        resolve()
        console.log('D')
    })
}

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

async1()

new Promise((resolve) => {
    console.log('G')
    resolve()
}).then(() => {
    console.log('H')
}).then(() => {
    console.log('I')
})
console.log('J')
正确输出顺序(下方拖动鼠标查看答案)↓
E 、 A 、 C 、 D 、G 、J 、B 、H 、I 、F

4. 推荐阅读学习

Tasks, microtasks, queues and schedules

深入介绍了MutationObserver在EventLoop的应用场景,感兴趣的可以去了解学习