EventLoop(事件循环)

概念:

首先我们需要知道JS是一种单线程语言,简单的说就是:只有一条通道,那么在任务多的情况下,就会出现拥挤的情况,这种情况下就产生了 ‘多线程’ ,但是这种“多线程”是通过单线程模仿的,也就是假的。那么就产生了同步任务和异步任务。

eventLoop

导图要表达的内容用文字来表述的话:

  • 同步和异步任务分别进入不同的执行”场所”,同步的进入主线程,异步的进入Event Table并注册函数。
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue(任务队列)。
  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
  • 事件循环中,每进行一次循环操作称为tick。

何为异步

代码在执行过程中,会遇到一些无法立即处理的任务,比如:

  • 计时完成后需要执行的任务 :setTimeoutsetInterval`;
  • 网络通信完成后需要执行的任务 : XHRFetch
  • 用户操作后需要执行的任务: addEventListener
  • 如果让渲染主线程等待这些任务的时机达到,就会导致主线程长期处于「阻塞」的状态,从而导致浏览器「卡死」;
  • 渲染主线程承担着极其重要的工作,无论如何都不能阻塞,因此,浏览器选择异步来解决这个问题;
  • 使用异步的方式,渲染主线程永不阻塞

面试题:如何理解 JS 的异步?

参考答案:

JS是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。

而渲染主线程承担着诸多的工作,渲染页面、执行 JS 都在其中运行。

如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。

所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。

在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。

线程与进程

线程和进程是操作系统中的两个概念:

  • 进程(process):计算机已经运行的程序(程序运行需要有它自己专属的内存空间,可以把这块内存空间简单的理解为进程);
  • 线程(thread):操作系统能够运行运算调度的最小单位;

听起来很抽象,我们直观一点解释:

  • 进程:我们可以认为,启动一个应用程序,就会默认启动一个进程(每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意);
  • 线程:一个进程至少有一个线程,所以在进程开启后会自动创建一个线程来运行程序的代码,该线程称之为主线程;
  • 如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码,所以一个进程中可以包含多个线程;

再用一个形象的例子解释:

  • 操作系统类似于一个工厂;
  • 工厂中里有很多车间,这个车间就是进程;
  • 每个车间可能有一个以上的工人在工厂,这个工人就是线程;

多进程多线程开发

操作系统是如何做到同时让多个进程(边听歌、边写代码、边查阅资料)同时工作呢?

  • 这是因为CPU的运算速度非常快,它可以快速的在多个进程之间迅速的切换;
  • 当我们的进程中的线程获取获取到时间片时,就可以快速执行我们编写的代码;
  • 对于用于来说是感受不到这种快速的切换的;

进程与线程

浏览器多进程

浏览器内部工作极其复杂,为了避免相互影响,为了减少连环崩溃的几率,当启动浏览器后,它会自动启动多个进程。

它主要包括以下进程:

  • 浏览器进程:浏览器的主进程,唯一,负责创建和销毁其它进程、浏览器界面的展示、用户交互,前进后退等。

  • GPU 进程:用于 3D 绘制等,最多一个。

  • 网络进程:负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务。

  • 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建。

  • 浏览器渲染进程(浏览器内核):渲染进程启动后,会开启一个渲染主线程,主线程负责执行 HTML、CSS、JS 代码。默认情况下,浏览器会为每个标签页(Tab页)开启一个新的渲染进程,以保证不同的标签页之间不相互影响。

    • 每个进程中又有很多的线程,其中包括执行JavaScript代码的线程;

      但是JavaScript的代码执行是在一个单独的线程中执行的;这就意味着JavaScript的代码,在同一个时刻只能做一件事;如果这件事是非常耗时的,就意味着当前的线程就会被阻塞;

宏任务与微任务

宏任务是由宿主发起的,而微任务是由JS发起的。

名称 宏任务(macrotask) 微任务(microtask)
谁发起的 宿主(node、浏览器) JS引擎
谁先运行 后运行 先运行
会触发新一轮Tick吗 不会

事件循环中维护着两个队列

  1. 宏任务队列主要包括:
  • script(整体代码) ps:可以理解为外层同步代码
  • ajax
  • setTimeout
  • setInterval
  • UI交互事件
  • I/O(Node.js)
  • setImmediate(Node.js 环境)
  1. 微任务队列主要包括:
  • Promise的then回调
  • process.nextTick(Node.js)
  • Mutation Observer API
  • queueMicrotask()

宏任务与微任务是怎么执行的

  • main script中的代码优先执行(编写的顶层script代码);

  • 在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行;

  • 也就是宏任务执行之前,必须保证微任务队列是空的;

  • 如果不为空,那么就优先执行微任务队列中的任务(回调);

  • 总结:先执行同步代码,遇到异步宏任务则将异步宏任务放入宏任务队列中,遇到异步微任务则将异步微任务放入微任务队列中,当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将异步宏任务从队列中调入主线程执行,一直循环直至所有任务执行完毕。(每次执行宏任务之前,都会在微任务队列里检查有没有微任务,有的话就先把微任务队列里执行完,然后再执行此个宏任务)

宏微任务

案例

DEMO1:

1
2
3
4
5
6
7
8
9
10
11
12
13
setTimeout(function(){
console.log('1');
});
new Promise(function(resolve){
console.log('2');
resolve();
}).then(function(){
console.log('3');
}).then(function(){
console.log('4')
});
console.log('5');
// 2 5 3 4 1

分析:(其实先找同步的,然后再找微任务,再找宏任务比较快) ps:看第2条

  1. 遇到setTimout,异步宏任务,放入宏任务队列中
  2. 遇到new Promise,new Promise在实例化的过程中所执行的代码都是同步进行的,所以输出2
  3. 而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
  4. 遇到同步任务console.log(‘5’);输出5;主线程中同步任务执行完
  5. 从微任务队列中取出任务到主线程中,输出3、 4,微任务队列为空
  6. 从宏任务队列中取出任务到主线程中,输出1,宏任务队列为空

DEMO2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
setTimeout(()=>{
new Promise(resolve =>{
resolve();
}).then(()=>{
console.log('test');
});

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);
//1 2 3 before timeout also before timeout 4 test

分析:

  1. 看到setTimeout,为宏任务,直接不用管了,往下找
  2. 看到new Promise,new Promise在实例化的过程中所执行的代码都是同步进行的,所以输出1
  3. 而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
  4. 遇到同步任务console.log(‘2’);输出2;主线程中同步任务执行完
  5. 微任务队列中取出任务到主线程中,输出3,此微任务中又有微任务,Promise.resolve().then(微任务a).then(微任务b),将其依次放入微任务队列中
  6. 从微任务队列中取出任务a到主线程中,输出 before timeout;
  7. 从微任务队列中取出任务b到主线程中,任务b又注册了一个微任务c,放入微任务队列中;
  8. 从微任务队列中取出任务c到主线程中,输出 also before timeout;微任务队列为空
  9. 从宏任务队列中取出任务到主线程,此任务中注册了一个微任务d,将其放入微任务队列中,接下来遇到输出4,宏任务队列为空
  10. 从微任务队列中取出任务d到主线程 ,输出test,微任务队列为空

DEMO3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
  console.log('1');

setTimeout(function () {
console.log('2');
process.nextTick(function () {
console.log('3');
})
new Promise(function (resolve) {
console.log('4');
resolve();
}).then(function () {
console.log('5')
})
})

process.nextTick(function () {
console.log('6');
})

new Promise(function (resolve) {
console.log('7');
resolve();
}).then(function () {
console.log('8')
})

setTimeout(function () {
console.log('9');
process.nextTick(function () {
console.log('10');
})
new Promise(function (resolve) {
console.log('11');
resolve();
}).then(function () {
console.log('12')
})
})
//1 7 6 8 2 4 3 5 9 11 10 12

分析:(大白话版)

  1. 同步任务直接输出 1
  2. 遇到setTimeout,加入宏任务队列(后执行的那种,整个函数就先不用看了)
  3. process.nextTick,加入微任务队列
  4. new Promise,new Promise在实例化的过程中所执行的代码都是同步进行的,所以输出7
  5. 而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
  6. 遇到setTimeout,加入宏任务队列(后执行的那种,整个函数就先不用看了) 主线程中同步任务执行完
  7. 找微任务,因为是栈,所以先入先出,所以从上往下找,process.nextTick 输出 6
  8. new Promise.then 输出 8
  9. 找宏任务;在宏任务里面先找同步代码 输出 2 、4;然后找微任务:输出3、5
  10. 找宏任务;在宏任务里面先找同步代码 输出 9、11;然后找微任务:输出10、12

DEMO4:

async await是promise的语法糖

  • 我们可以将await关键字后面执行的代码,看做是包裹在(resolve,reject)=>{函数执行}的代码;
  • await 的下一条语句,可以看做是then(res=>{函数执行})中的代码;

async执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}

async function async2() {
console.log("async2");
}

console.log("script start");

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

async1();

new Promise((resolve) => {
console.log("promise1");
resolve();
}).then(() => {
console.log("promise2")
})

console.log("script end");
//script start async1 start async2 promise1 script end async1 end promise2 setTimeout

分析:

  1. 遇到函数 async1,async2定义,不会执行,不用管;
  2. 同步任务:输出 “script start”
  3. setTimeout,加入宏任务队列;
  4. async1()调用,输出 “async1 start”;下一行:await async2();相当于promise里面的resolve包裹的代码,直接执行,输出”async2”;下一行 :console.log(“async1 end”);相当于promise中的then,加入微任务队列;
  5. 输出”promise1”,resolve()调用.then中的代码,但是为微任务,加入微任务队列;
  6. 输出”script end”
  7. 在微任务队列中查找,不为空,依次执行队列:输出”async1 end”;输出”promise2”
  8. 在宏任务队列中查找,检查微任务队列是否为空,为空,执行此个宏任务,输出”setTimeout”

参考资料