浏览器事件循环机制 (Event Loop)

浏览器事件循环机制

JS 是单线程的,非阻塞的。
虽然 JS 运行在浏览器中是单线程的,但是浏览器是事件驱动的(Event Driven), 浏览器中很多行为是异步的 (Asynchronized), 会创建事件并放入执行队列。
浏览器中很多异步行为都是由浏览器中的线程去完成,主要由以下 3 个线程去执行:

1.JS 引擎线程
2.GUI 渲染线程
3.事件触发线程

JS 非阻塞的主要通过 Event Loop 来实现。

浏览器的事件循环

浏览器中的事件循环 Event Loop,分为同步执行栈和异步队列,首先会执行同步的任务,当同步任务执行完之后会从异步队列中取异步任务拿到同步执行栈中进行执行。

执行栈和事件队列

JavaScript 引擎线程专门处理 JavaScript 脚本,JS 引擎主要由 2 个组件构成

1.堆(Memory Heap) - 内存分配发生的地方
2.栈(Call Stack) - 函数调用时会形成一个个栈帧(frame)

可以使用Loupe 工具来了解 JavaScript 的调用堆栈、事件循环、回调队列如何执行的。

执行栈

每个函数执行的时候都会生成新的 execution context(执行上下文),执行上下文会包含一些当前函数的参数、局部变量,它会被推入栈中,running execution context(正在执行的上下文)始终处于栈的顶部。当函数执行完后,它的上下文会从栈弹出。

事件队列 FIFO队列

先执行同步代码,遇到异步代码的执行,不会等待异步事件结果返回,而是将事件挂起,继续执行执行栈中的其他任务。
当异步事件返回结果,将它放到事件队列中,等待执行栈中的任务全部执行完成,然后主线程空闲状态,主线程会去查找事件队列中的任务,取第一个事件,然后把事件回调放到执行栈中,然后执行同步代码。

同步代码属于宏任务会开始执行一次,不同的异步任务又被分为宏任务和微任务。

宏任务(Macro Task)和微任务(Micro Task)

宏任务

优先级低,先定义的先执行。包括:ajax,click,setTimeout,setInterval,事件绑定,postMessage,MessageChannel

微任务

微任务,优先级高,并且可以插队,不是先定义先执行。 包括:promise 中的 then,observer,MutationObserver,setImmediate。

宏任务包括 Script, setTimeout/setInterval, setImmediate, postMessage, I/O 和 UI rendering。
微任务包括 Promise.then(), Object.observe 和 Mutation.Observer。

说明:
setImmediate 非标准
Object.observe 已废弃
Mutation.Observer

运行机制

异步任务的返回结果会被放到一个任务队列中,根据异步事件的类型,将结果放到对应的宏任务和微任务队列中去。
执行栈为空时,=> 查找微任务队列事件 => 宏任务队列事件。

经典题目分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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');

1.执行同步代码,输出 script start
2.遇到 setTimeout,将回调压入宏任务队列
3.执行 async1,输出 async1 start,然后执行 async2,输出 async2,将 await async2() 后的代码压到微任务队列中
4.执行 Promise,输出 promise1, 将 Promise.then() 压入到微任务队列中。
5.输出 script end ,至此同步代码执行完成
6.执行微任务队列中的代码,输出 async1 end,然后输出 promise2,此时微任务队列执行完成。
7.执行宏任务队列代码,输出 setTimeout

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

new Promise(function(resolve, reject) {
console.log('children4');
setTimeout(function() {
console.log('children5');
resolve('children6')
}, 0)
}).then((res) => {
console.log('children7');
setTimeout(() => {
console.log(res);
}, 0)
})

1.执行同步代码,输出 start
2.setTimeout 压入宏任务队列1
3.遇到 Promise, 输出 children4,遇到 setTimeout 压入宏任务队列2,Promise.then 还没执行。
4.执行宏任务队列1,输出 children2
5.执行宏任务队列1中的微任务 Promise.then(),输出 children3
6.执行宏任务队列2,输出 children5,并 resolve 执行 Promise.then,输出 children7
7.遇到 setTimeout 压入宏任务队列,此时微任务队列没任务,执行宏任务,输出 children6

参考

浏览器事件循环机制(event loop)
JavaScript中的Event Loop(事件循环)机制
深入理解js事件循环机制(浏览器篇)