进程_线程_js事件循环

进程与线程

进程(process)

计算机已经运行的程序,是操作系统管理程序的一种方式;我们可以认为:启动一个程序,就会启动一个或者多个进程。

线程(thread)

操作系统能够运行运算调度的最小单位,通常情况下它被包含在进程中(进程是线程的容器);同样,我们可以认为:每一个进程,都会至少启动一个线程来执行程序中的代码,这个线程被称为主线程。

当进程中的线程获取到时间片时,就可以快速执行我们编写的代码。

js线程

JavaScript是单线程的,但是JavaScript的线程应该有自己的容器进程︰浏览器或者Node。

  • 目前多数的浏览器其实都是多进程的,当我们打开一个tab页面时就会开启一个新的进程
    • 如果浏览器为单进程,一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出;
  • 每个进程中又有很多的线程,其中包括执行JavaScript代码的线程;
    • 如果当前js线程时耗时操作,就会造成代码阻塞。
    • 解决方法:
      • 浏览器的每个进程是多线程的,那么其他线程可以来完成这个耗时的操作;比如网络请求、定时器,我们只需要在特性的时候执行应该有的回调即可。

事件循环

在浏览器环境下,js单线程属于浏览器进程;在Node环境下,js单线程属于Node进程。

js本身是单线程的,意味着js不适合做耗时操作,否则会导致程序阻塞;

那么,耗时操作就交给浏览器或者Node创建的其他线程来完成,并将处理结果以回调函数的形式,放入浏览器或者Node维护的事件队列中,js在合适的时机来执行这些回调函数;

js在执行回调函数时,也有可能会遇见耗时操作,同理,这些耗时操作一就会被加入到事件队列中已在合适的时机被js执行。

宏任务&&微任务

宏任务(macrotask)

浏览器的宏任务:ajax回调、定时器、DOM事件回调、UI Rendering(UI渲染完之后的回调);

Node的宏任务:setTimeout、setInterval、IO事件、setImmediate、close事件。

微任务(microtask)

浏览器的微任务:queueMicrotask的回调、promise.then的回调、MutationObserver的API;

Node的微任务:Promise的then回调、process.nextTick的回调、queueMicrotask 的回调。

但是,Node中不简单是一个宏任务和微任务队列,

微任务队列:

  • nextTick queue:process.nextTick
  • other queue:Promise的then回调,queueMicrotask

宏任务队列:

  • timer queue : setTimeout、setInterval ;
  • poll queue : IO事件;
  • check queue : setImmediate ;
  • close queue : close事件;

Node的宏任务和微任务的执行顺序:nextTick queue、other queue、timer queue、poll queue、check queue、close queue

总的执行过程

注意:在执行宏任务之前,需要确保问任务队列被清空;new Promise是同步代码。

  1. 首先执行js顶层代码(同步代码);
  2. 执行的过程中,遇见宏任务,将该宏任务加入到宏任务队列;
  3. 遇见微任务,将该微任务加入到微任务队列;
  4. 顶层代码执行完毕,依次执行微任务队列中的任务,清空微任务队列;
  5. 执行宏任务队列最先加入的宏任务,同时执行步骤2、3;
  6. 执行微任务队列中的任务,清空微任务队列,执行步骤5;

浏览器

浏览器维护着两个队列:宏任务队列和微任务队列。

浏览器的事件循环(简):

浏览器事件循环

Node

浏览器中的EventLoop是根据HTML5定义的规范来实现的,不同的浏览器可能会有不同的实现,而Node中是由libuv实现的。

Node架构图:

Node架构图

  • libuv中主要维护了一个EventLoop和worker threads(线程池),其主要是为Node开发的,现在也被使用到Luvit、Julia、pyuv等其他地方;
  • EventLoop负责调用系统的一些其他操作∶文件的IO、Network、child-processes等

服务器相对于浏览器最大的区别:I/O操作。

js本身并不会进行发起网络请求,连接数据库写入数据库,读取文件等操作,这就交给libuv来执行,相当于libuv给js提供了一些接口供其来调用,其本质是libuv执行了系统调用,并将执行结果以回调函数的形式返回,js在合适时机从事件队列拿取这些回调函数去处理结果。

Node中的事件循环被划分为很多阶段:

  • 定时器(Timers):本阶段执行已经被setTimeout()和setInterval()的调度回调函数。
  • 待定回调(Pending Calback):对某些系统操作(如TCP错误类型)执行回调,比如TCP连接时接收到ECONNREFUSED。
  • idle, prepare:仅系统内部使用。
  • 轮询(Poll )∶检索新的IO事件;执行与I/O相关的回调;
  • 检测( check ) : setImmediate()回调函数在这里执行。
  • 关闭的回调函数:一些关闭的回调函数,如:socket.on(‘close’,…)。

其中,Node程序经常停留在I/O阶段

Node微任务队列

例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Promise.resolve().then(() => {
console.log(0);
return 4
}).then((res) => {
console.log(res)
})

Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() => {
console.log(6);
})
// 0 1 4 2 3 5 6
// 以上代码不做分析

稍稍修改:

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

Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() => {
console.log(6);
})

// 0 1 2 4 3 5 6

与第一次的不同之处在于,console.log(0)之后返回了一个thenable对象(实现了then方法的对象)
在原生的Promise的实现中,返回的thenable对象如果resolve了,这个resolve会被往后推一次,推到下一次的微任务里面

图示:

以上代码图示

参看:

https://ke.qq.com/webcourse/3619571/103765593#taid=11926213651544819&vid=8602268011107553485

时间:1:10

再稍稍修改一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Promise.resolve().then(() => {
console.log(0);
return Promise.resolve(4)
}).then((res) => {
console.log(res)
})

Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() => {
console.log(6);
})

// 0 1 2 3 4 5 6

分析:返回了一个thenable对象会被退后一次,即:不是普通的值会被推后一次

如果返回了return Promise.resolve(4)

则会推后两次。