什么是事件循环

简单地概括,事件循环就是JS调度同步、异步任务的策略

同步任务意思就是代码是同步执行的,异步任务则是代码是异步执行的,因为JS是单线程的,所以如果JS全部采用同步任务的方式,那么遇到setTimeout这种语句,得等时间走完才能执行下面的内容

事件循环的方式就是,直接执行同步任务,将异步任务交付给其它线程,当异步任务执行完后往事件队列里面塞一个回调函数,当执行栈为空时,主线程才会去读取事件队列,看看有没有任务(异步任务执行完的回调)要执行,每次取一个来执行,重复直到事件队列为空

知道这一点,以下代码就很容易得出,先输出同步任务,再输出异步任务的结论

1
2
3
4
setTimeout(() => {
console.log('异步任务');
}, 0);
console.log('同步任务');

宏任务与微任务

首先来说明一下why,为什么有宏任务和微任务之分,有的时候,一些异步操作并不想要经历整个事件循环,所以有了微任务,与之相对的就是宏任务

宏任务(Marco)和微任务(Micro)的执行顺序是,第一次事件循环,整段代码作为宏任务进入主线程执行,同步代码被直接推到执行栈执行,遇到异步代码就挂起交由其他线程执行(执行完会往事件队列塞回调),同步代码执行完,读取微任务队列,若有则执行所有微任务,微任务清空,页面渲染,从事件队列面里取一个宏任务塞入执行栈执行,重复上述过程

1
2
3
4
5
6
7
8
9
10
11
# 宏任务
for (let macrotask of macrotask_list) {
# 执行一个宏任务
macrotask();
# 执行所有微任务
for (let microtask of microtask_list) {
microtask();
}
# UI渲染
render();
}

宏任务和微任务的划分如下

任务 Chrome Node 分类
I/O Marco
requestAnimationFrame × Marco
setTimeout Marco
setInterval Marco
setImmediate × Marco
process.nextTick × Micro
MutationObserver × Micro
Promise Micro

ok,先来看一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
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加入事件队列,执行,输出script start和script end,然后清理微任务队列,输出promise1, promise2,最后输出setTImeout

再看一个稍微复杂的例子

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

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

new Promise(resolve => {
console.log('promise1');
resolve();
setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
console.log('then1')
})

console.log('script end');

输出的顺序是

1
2
3
4
5
6
script start
promise1
script end
then1
timeout1
timeout2

promise本身的内容是立即执行的,只有then中的部分才属于micro task

再来看一个终极版本

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
console.log("script start")

$(window).click(() => {
console.log('clicked1');
Promise.resolve().then(() => console.log('clicked promise1'));
setTimeout(() => console.log('clicked timeout1'), 0);
});

$(window).click(() => {
setTimeout(() => {
console.log('clicked2');
Promise.resolve().then(() => console.log('clicked promise2'));
setTimeout(() => console.log('clicked timeout2'), 0);
}, 0);
});

$(window).click(() => {
Promise.resolve().then(() => {
console.log('clicked3');
Promise.resolve().then(() => console.log('clicked promise3'));
setTimeout(() => console.log('clicked timeout3'), 0);
});
});

console.log("script end")

首先会输出script start和script end,然后点击window,将click1加入主执行栈,clicked promise1,第三个Promise加入微任务队列,第一个和第二个setTimeout加入事件队列

第一次执行输出click1,并清理微任务队列,输出clicked promise1,clicked3,继续执行微任务输出clicked promise3,将第三个setTimeout加入事件队列

执行宏任务clicked timeout1,无微任务

执行第二个setTimeout,输出clicked2,将clicked promise2加入微任务,将clicked timeout2加入事件队列,清理微任务,输出clicked promise2

执行宏任务clicked timeout2,无微任务 (顺序执行)

执行宏任务clicked timeout3,无微任务

总体顺序如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
script start

script end

clicked1

clicked promise1

clicked3

clicked promise3

clicked timeout1

clicked2

clicked promise2

clicked timeout2

clicked timeout3

交换第二部分和第三部分的代码,就会交换clicked timeout2和clicked timeout3的执行顺序