JS 事件循环机制

Posted by Rimin on 2019-04-30

同步和异步

异步:简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。比如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步。
同步:相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。

单线程

任务队列

因为js是单线程,任务需要排队,但是如果有一个任务要花费很长时间,这时后面的任务就会执行不了,造成阻塞,所以,设计者们意识到这个问题后,设计了一个方法:这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

“任务队列” 是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

“任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列”,等待主线程读取。

⚠️ 注意:在HTML Living Standard中描述到事件循环的步骤:

An event loop has one or more task queues.(事件循环具有一个或多个任务队列)

  1. Let oldestTask be the oldest task on one of the event loop’s task queues
  2. Set the event loop’s currently running task to oldestTask
  3. Run oldestTask
  4. Set the event loop’s currently running task back to null
  5. Remove oldestTask from its task queue
  6. Microtasks: Perform a microtask checkpoint
  7. Update the rendering(重新渲染页面)
  8. If this is a worker event loop (i.e. one running for a WorkerGlobalScope), but there are no tasks in the event loop’s task queues and the WorkerGlobalScope object’s closing flag is true, then destroy the event loop, aborting these steps, resuming the run a worker steps described in the Web workers section below.

Event Loop

image

(图片来自Philip Roberts的演讲《Help, I’m stuck in an event-loop》)

microtask 和 macroTask

在每一次事件循环中,macrotask 只会提取一个执行,而 microtask 会一直提取并且按优先级顺序执行,直到 microtasks 队列清空.

那么一般哪些会被推入macrotask,哪些会被推入microtask呢?

1
2
3
4
5
- macrotasks:  script (整体代码),setTimeout, setInterval, setImmediate, I/O, UI rendering

- microtasks: process.nextTick(node), Promises, Object.observe(废弃), MutationObserver

以上为按优先级排序

接下来看几个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
console.log('script start');

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

Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});

console.log('script end');

//执行结果:
script start
script end
promise1
promise2
setTimeout

⚠️注意!由于the link between jobs & microtasks is vague. 有关微任务的概念没有较为标准的规定,以及promises come from ECMAScript rather than HTML。ECMAScript has the concept of “jobs” which are similar to microtasks, but the relationship isn’t explicit aside from vague mailing list discussions。因此实际执行的顺序经过试验,chrom是最正确的,而其他包括FF,sfari,ie都有可能有出入。

再来看这个例子

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
 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
// promise2
// setTimeout

上面的 async 函数实际上是, 一个语法糖

1
2
3
4
5
6
7
new Promise((resolve, reject) => {
console.log("async1 start");
console.log("async2");
resolve(Promise.resolve());
}).then(() => {
console.log("async1 end");
});

墙裂推荐看这篇博文Tasks, microtasks, queues and schedules,里面的例子很经典(有坑)!!

⚠️ We don’t process the microtask queue between listener callbacks, they’re processed after both listeners.