javascript 执行机制

JavaScript 是单线程的,即只有一个调用栈,调用栈按照先入后出的规则执行,一次调用一个,而且可以嵌套,在执行调用栈的时候回先执行同步的任务,同步任务马上就能执行,调用栈在发现异步任务的时候回把异步任务放入队列里面,异步任务队列分为宏任务队列微任务队列,队列都按照先入先出的规则

同步任务和异步任务

flowchart TB
A(任务进入执行栈)--> B{同步任务还是异步任务}
B --> |同步| C["主线程"]
C --> D["任务全部执行完毕"]
D --> E["读取任务队列中的结果,进入主线程执行"]

B --> |异步| F[Event Table]
F --> |注册回调函数| G[Event Queue]
G <--> E
classDef cdfece fill:#cdfece,stroke:#333,stroke-width:2px;
classDef d1e1fe fill:#d1e1fe,stroke:#333,stroke-width:2px;
classDef e5cdfe fill:#e5cdfe,stroke:#333,stroke-width:2px;
class A,E cdfece;
class C,D d1e1fe;
class F,G e5cdfe;
style B fill:#fd999a,stroke:#333,stroke-width:2px;
  • 同步和异步任务分别进入不同的执行场所,同步的进入主线程,异步的进入 Event Table 并注册函数。
  • 当指定的事情完成时,Event Table 会将这个函数移入 Event Queue。
  • 主线程内的任务执行完毕为空,会去 Event Queue 读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的 Event Loop(事件循环)。

宏任务和微任务

  1. 宏任务队列
  • 新程序或子程序被直接执行
    最常见的就是<script>元素里面的代码就是程序被执行,控制台写代码也算是程序执行
  • 事件的回调函数
    如:鼠标点击事件触发后,里面的回调函数就会被添加到宏任务队列
  • setTimeout()和 setInterval()
  • requestAnimationFrame
  • I/O 操作
  • setImmediate
  • UI rendering
  1. 微任务队列
  • Promise.then().catch().finally()
  • MutationObserver
  • Object.observe
  • Node.js 中的 process.nextTick()

事件循环

要知道如何运行任务,需要靠 Event Loop 事件循环,事件循环是一个不断进行循环的机制,事件循环会不断去寻找可以执行的任务来执行,在执行完同步任务以后,也就是清空了调用栈以后,首先会执行微任务队列的任务,把微任务队列的任务清空以后才会去执行宏任务。在进行下一步宏任务之前,浏览器可能会发生渲染的情况,在渲染以后再执行宏任务。

注意: 并不是每个浏览器都把这些函数归为同一类宏任务或微任务,有些浏览器会把 Promise.then()归为宏任务

浏览器运行任务的顺序

  1. 宏任务 –> 微任务 –> 渲染 –> 宏任务
  2. 事件循环
  3. 宏任务 –> 微任务 –> 渲染 –> 宏任务
  4. 事件循环

首先执行宏任务,因为所有 js 代码都是在 script 中的,在调用栈为空的时候,事件循环优先执行微任务,并且清空微任务队列才会看浏览器是否需要渲染,渲染过后事件循环再执行下一轮宏任务,如果此时宏任务的其中一轮结束后,也就是调用栈清空的时候,事件循环有发现微任务,还会执行微任务,并且清空微任务队列才会看一下是否需要渲染,渲染过后事件循环再执行下一轮宏任务,在没有其余微任务的时候,宏任务就可以一直清空宏任务队列了

宏任务的执行顺序

  1. timers 定时器:执行已经安排的 setTimeout 和 setInterval 的回调函数
  2. pending callback 待定回调:执行延迟到下一个循环迭代的 I/O 回调
  3. idle,prepare:仅系统内部使用
  4. poll:检索新的 I/O 事件,执行与 I/O 相关的回调
  5. check:执行 setImmediate() 回调函数
  6. close callbacks:socket.on("close", () => {})

微任务和宏任务在 node 的执行顺序

node v10 及以前

  1. 执行完一个阶段中的所有任务
  2. 执行 nextTick 队列里的内容
  3. 执行完微任务队列的内容

node v10 以后

和浏览器的行为统一

实例

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
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}

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

console.log("script start");

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

async1();

new Promise(function (resolve) {
console.log("promise1");

resolve();
}).then(function () {
console.log("promise2");
});

console.log("script end");

// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// setTimeout
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
console.log("start");

setTimeout(() => {
console.log("children1");

Promise.resolve().then(() => {
console.log("children2");
});
}, 0);

new Promise(function (resolve, reject) {
console.log("children3");

setTimeout(() => {
console.log("children4");
resolve("children5");
}, 0);
}).then(res => {
console.log("children6");

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

// start
// children3
// 第一轮宏任务结束,尝试清空微任务队列,发现没有微任务
// children1
// 第二轮宏任务结束,尝试清空微任务队列
// children2
// children4
// children6
// children5