九、操作系统中的进程和线程及浏览器事件循环中的宏任务微任务队列的深入学习

2022/8/10 ES6进程线程浏览器事件循环微任务队列宏任务队列Node中的事件循环

# 1、操作系统中的进程和线程

# 1.1.进程和线程

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

    • 进程(process):计算机已经运行的程序,是操作系统管理程序的一种方式;
    • 线程(thread):操作系统能够运行运算调度的最小单位,通常情况下它被包含在进程中;
  • 听起来很抽象,这里还是给出我的解释:

    • 进程:我们可以认为,启动一个应用程序(如:chrome浏览器),就会默认启动一个进程(也可能是多个进程);【chrome浏览器是多进程的(每当我们打开一个tab页面时就会开启一个新的进程),而每个进程里面又是多线程的;】
    • 线程:每一个进程中,都会启动至少一个线程用来执行程序中的代码,这个线程被称之为主线程;
    • 所以我们也可以说进程是线程的容器;
  • 再用一个形象的例子解释:

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

# 1.2.操作系统 – 进程 – 线程

v1KP2t.png

# 1.3.操作系统的工作方式

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

    • 这是因为CPU的运算速度非常快,它可以快速的在多个进程之间迅速的切换;
    • 当我们进程中的线程获取到时间片时,就可以快速执行我们编写的代码;
    • 对于用户来说是感受不到这种快速的切换的;
  • 你可以在Mac的活动监视器或者Windows的资源管理器中查看到很多进程:

v1KkKf.png

# 2、浏览器中的JavaScript线程

  • 我们经常会说JavaScript是单线程(可以开启workers)的,但是JavaScript的线程应该有自己的容器进程:浏览器或者Node

  • 浏览器是一个进程吗,它里面只有一个线程吗?

    • 目前多数的浏览器其实都是多进程的,当我们打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出;
    • 每个进程中又有很多的线程,其中包括执行JavaScript代码的线程
  • JavaScript的代码执行是在一个单独的线程中执行的:

    • 这就意味着JavaScript的代码,在同一个时刻只能做一件事;
    • 如果这件事是非常耗时的,就意味着当前的线程就会被阻塞;
  • 所以真正耗时的操作,实际上并不是由JavaScript线程在执行的

    • 浏览器的每个进程是多线程的,那么浏览器的其他线程可以来完成这个耗时的操作
    • 比如网络请求、定时器,我们只需要在特定的时候执行应该有的回调即可;

# 3、浏览器的事件循环

  • 如果在执行JavaScript代码的过程中,有异步操作呢?
    • 中间我们插入了一个setTimeout的函数调用;
    • 这个函数被放到入调用栈中,执行会立即结束,并不会阻塞后续代码的执行;
      • setTimeout定时器函数执行完之后;里面的回调函数需要等待1s后才会执行;为什么不会阻塞后续代码的执行呢?
      • 前面已经讲到了如果是一个耗时操作,实际上该操作并不是由JavaScript线程在执行的;而是浏览器的其他线程来帮助我们来完成这个耗时操作(记录定时时间,时间到达后将定时器函数中的回调函数,加入到宏任务队列中等待执行:具体什么时候执行:下面4、宏任务和微任务 有说明);
function sum(num1,num2) {
  return num1 + num2
}
function bar() {
  return sum(20,30)
}
setTimeout(()=> {
  console.log('setTimeout')
},1000)

const result = bar()
console.log(result)

/**  上面代码执行打印结果如下:
 * 50
 * setTimeout
 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

v1KrqO.png

  • 什么是事件循环;总结:
    • js是一门单线程语言,同一时间只能做一件事,单线程可能会出现阻塞问题;所以js分了同步任务和异步任务;同步任务会进入主线程,也就是在主执行栈中立即执行;异步任务会交给浏览器去管理,随后进入任务队列中;当主线程中所有的同步任务执行完以后,读取任务队列中的异步任务并拿到主线程中去执行;以上的操作不断的重复就叫做事件循环
  • js处理不了异步任务,所以它要交给宿主环境去处理,常见的宿主环境有:浏览器和node环境;宿主环境都会有事件循环进行处理

# 4、宏任务和微任务

  • 但是事件循环中并非只维护着一个队列,事实上是有两个队列:
    • 宏任务队列(macrotask queue):ajaxsetTimeoutsetIntervalDOM监听UI Rendering -> UI界面渲染
    • 微任务队列(microtaskqueue):Promise的then回调Mutation Observer APIqueueMicrotask(fn)方法-> 将一个函数加入到微任务队列中执行
      • 注意:在异步函数async中,有await语句的 该行代码的赋值操作及后面的代码,也是会被加到微任务队列中
//注意:在异步函数async中,有await语句的  该行代码的赋值操作及后面的代码,也是会被加到微任务队列中
console.log('script start')

setTimeout(()=> {//宏任务
  console.log('setTimeout')
},0)

async function foo() {
  const result = await 'kobe'//改行代码的赋值操作及后续代码为微任务
  console.log(result)
}
foo()
console.log('script end')

/** 上面代码打印结果如下:
 * script start
 * script end
 * kobe
 * setTimeout
 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  • 那么事件循环对于两个队列的优先级是怎么样的呢?

    1. 首先:main script中的代码优先执行(编写的顶层script代码 - 全局代码);
    2. 然后 :在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行
      • 也就是宏任务执行之前,必须保证微任务队列是空的(微任务执行完,才能执行宏任务);
      • 如果不为空,那么就优先执行微任务队列中的任务(回调);
  • 补充:事件队列中的任务执行,是将任务 压入到执行上下文栈中进行执行的(具体可以看 3、浏览器的事件循环 下面的图,进行理解)

  • 下面我们通过几到面试题来练习一下。

# 5、Promise面试题(考察微任务宏任务知识)

console.log("script start")//第1位打印

setTimeout(function () {//宏任务1 -> 宏任务队列(先进先出):宏任务1 
  console.log("setTimeout1");//第8位打印
  new Promise(function (resolve) {
    resolve();
  }).then(function () {//微任务(1): 之前微任务队列中的微任务都执行完了,微任务队列中只有微任务(1)了,所以我直接执行
    new Promise(function (resolve) {
      resolve();
    }).then(function () {//微任务[1]: 之前微任务队列中的微任务都执行完了,微任务队列中只有微任务[1]了,所以我直接执行
      console.log("then4");//第10位打印
    });
    console.log("then2");//第9位打印
  });
});

new Promise(function (resolve) {
  console.log("promise1");//第2位打印
  resolve();
}).then(function () {//微任务1 -> 微任务队列(先进先出):微任务1
  console.log("then1");//第5位打印 
});

setTimeout(function () {//宏任务2 -> 宏任务队列(先进先出):宏任务1 宏任务2
  console.log("setTimeout2");//第11位打印
});

console.log(2);//第3位打印

//queueMicrotask(fn)该方法是 传入一个回调函数;并直接执行该回调函数; 但是传入的回调函数 是加入到微任务队列中,进行执行的
queueMicrotask(() => {//微任务2 -> 微任务队列(先进先出):微任务1 微任务2
  console.log("queueMicrotask1")//第6位打印 
});

new Promise(function (resolve) {
  resolve();
}).then(function () {//微任务3 -> 微任务队列(先进先出):微任务1 微任务2  微任务3
  console.log("then3");////第7位打印 
});

console.log("script end")//第4位打印 

/** 以上代码执行后打印结果如下:
 * script start
 * promise1
 * 2
 * script end    (这里已经执行完 顶层的script代码了,下面开始执行微任务队列中的任务)
 * then1  【微任务1】
 * queueMicrotask1 【微任务2】
 * then3  【微任务3】   (这里已经执行完 微任务队列中的任务了,下面开始执行宏任务队列中的任务)
 * setTimeout1  【宏任务1】 (这里执行宏任务,发现里面产生了新的微任务,所以下面先将微任务队列中的任务执行完 再执行宏任务队列中的任务)
 * then2  【微任务(1)】   (这里执行微任务,发现又产生了新的微任务,那么下面继续执行新产生的微任务)
 * then4  【微任务[1]】   (这里微任务队列中的任务已经执行完成,下面开始执行宏任务队列中的任务)  
 * setTimeout2  【宏任务2】
 */
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# 6、promise async await 面试题(考察微任务宏任务知识)

async function async1 () {
  console.log('async1 start')//第2位打印
  const result = await async2();//微任务1(该行代码的赋值操作及后面的代码是微任务) -> 微任务队列(先进先出):微任务1
  console.log(result,'async1 end')//第6位打印
}

async function async2 () {
  console.log('async2')//第3位打印
  return 'wrw'
}

console.log('script start')//第1位打印

setTimeout(function () {//宏任务1 -> 宏任务队列(先进先出):宏任务1 
  console.log('setTimeout')//第8位打印
}, 0)

async1();

new Promise (function (resolve) {
  console.log('promise1')//第4位打印
  resolve();
}).then (function () {////微任务2  -> 微任务队列(先进先出):微任务1 微任务2
  console.log('promise2')//第7位打印
})

console.log('script end')//第5位打印

/** 以上代码执行后打印结果如下:
 * script start
 * async1 start
 * async2
 * promise1
 * script end    (这里已经执行完 顶层的script代码了,下面开始执行微任务队列中的任务)
 * wrw async1 end 【微任务1】
 * promise2    【微任务2】  (这里已经执行完 微任务队列中的任务了,下面开始执行宏任务队列中的任务)
 * setTimeout  【宏任务1】
 */
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
  • 测试案例(拓展)
setTimeout(() => {//宏任务1 -> 宏任务队列(先进先出):宏任务1 
  console.log('1')//第1位打印
  setTimeout(() => {//宏任务3 -> 宏任务队列(先进先出):宏任务1(执行完了产生新的宏任务:宏任务3) 宏任务2 宏任务3
    console.log('3')//第3位打印
    new Promise(resolve=> {
      resolve('kobe')
    }).then(res => {//微任务
      console.log(res)//第4位打印
    })
  });
},1000);

setTimeout(() => {//宏任务2 -> 宏任务队列(先进先出):宏任务1 宏任务2
  console.log('2')//第2位打印
},1000);

/** 以上代码执行后打印结果如下:
 * 1 【宏任务1】
 * 2 【宏任务2】
 * 3 【宏任务3】
 * kobe 【微任务】
 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 7、Node的事件循环 (后续学习node再学习)

最后更新时间: 2022/08/21, 17:31:13
彩虹
周杰伦