Event Loop 事件循环
Event Loop
即事件循环,是指浏览器或Node
的一种解决javaScript
单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。事件循环是
javaScript
实现异步的一种方法,也是js的执行机制。
为什么 JavaScript 是单线程?
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准? 所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
任务队列
单线程
就意味着,所有任务需要排队
,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。 所有任务可以分成两种,一种是同步任务(synchronous)
,另一种是异步任务(asynchronous)
。同步任务指的是,在主线程上排队执行的任务
,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务
,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。 具体来说,异步执行的运行机制如下:(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。
宏任务 & 微任务 都属于异步任务
这里需要注意的是
new Promise
是会进入到主线程中立刻执行,而promise.then
则属于微任务
- 宏任务(macro-task):
整体代码script
、setTimeOut
、setInterval
、I/O
、UI Rendering
- 微任务(mincro-task):
promise.then
、promise.nextTick(node、)
、MutationObserver
(具体使用方式查看👉这里)
EventLoop 事件循环
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
- 整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为两部分:“同步任务”、“异步任务”;
同步任务
会直接进入主线程依次执行
;异步任务
会再分为宏任务和微任务
;宏任务
进入到Event Table
中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;微任务
也会进入到另一个Event Table
中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;- 当
主线程内的任务执行完毕
,主线程为空时
,会检查微任务的Event Queue
,如果有任务,就全部执行
,如果没有就执行下一个宏任务
;上述过程会不断重复,这就是
Event Loop事件循环
;简单概况:js代码 执行时,先分同步和异步任务,先执行同步任务,依次执行,异步任务再分宏任务和微任务,都会进入到Event Table中,当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务;上述过程会不断重复,这就是Event Loop事件循环;
当当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。
详见下图:
事件循环
在主线程上添加宏任务与微任务
执行顺序:主线程 => 主线程上创建的微任务 => 主线程上创建的宏任务
console.log('start');
setTimeout(() => {
console.log('setTimeout'); // 将回调代码放入另一个宏任务队列
}, 0);
new Promise((resolve, reject) => {
for (let i = 0; i < 5; i++) {
console.log(i);
}
resolve()
}).then(()=>{
console.log('Promise实例成功回调执行'); // 将回调代码放入微任务队列
})
console.log('end')
# 浏览器运行结果
start
0
1
2
3
4
end
Promise实例成功回调执行
setTimeout
在微任务中创建微任务
执行顺序:主线程 => 主线程上创建的微任务1 => 微任务1上创建的微任务2 => 主线程上创建的宏任务
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
Promise.resolve().then(_ => {
console.log('before timeout')
}).then(_ => {
Promise.resolve().then(_ => {
console.log('also before timeout')
})
})
})
console.log(2)
# 浏览器运行结果
1
2
3
'before timeout'
'also before timeout'
4
宏任务中创建微任务
执行顺序:主线程 => 主线程上的宏任务队列1 => 宏任务队列1中创建的微任务
// 宏任务队列 1
setTimeout(() => {
// 宏任务队列 2.1
console.log('timer_1');
setTimeout(() => {
// 宏任务队列 3
console.log('timer_3')
}, 0)
new Promise(resolve => {
resolve()
console.log('new promise')
}).then(() => {
// 微任务队列 1
console.log('promise then')
})
}, 0)
setTimeout(() => {
// 宏任务队列 2.2
console.log('timer_2')
}, 0)
console.log('========== Sync queue ==========')
// 执行顺序:主线程(宏任务队列 1)=> 宏任务队列 2 => 微任务队列 1 => 宏任务队列 3
# 浏览器运行结果
'========== Sync queue =========='
'timer_1'
'new promise'
'promise then'
'timer_2'
'timer_3'
微任务队列中创建的宏任务
执行顺序:主线程 => 主线程上创建的微任务 => 主线程上创建的宏任务 => 微任务中创建的宏任务
// 宏任务1
new Promise((resolve) => {
console.log('new Promise(macro task 1)');
resolve();
}).then(() => {
// 微任务1
console.log('micro task 1');
setTimeout(() => {
// 宏任务3
console.log('macro task 3');
}, 0)
})
setTimeout(() => {
// 宏任务2
console.log('macro task 2');
}, 1000)
console.log('========== Sync queue(macro task 1) ==========');
# 浏览器运行结果
'new Promise(macro task 1)'
'========== Sync queue(macro task 1) =========='
'micro task 1'
'macro task 3'
'macro task 2'
记住,如果把setTimeout(() => { console.log('macro task 2'); }, 1000)改为立即执行setTimeout(() => { console.log('macro task 2'); }, 0)那么它会在macro task 3之前执行,因为定时器是过多少毫秒之后才会加到事件队列里
# 浏览器运行结果
'new Promise(macro task 1)'
'========== Sync queue(macro task 1) =========='
'micro task 1'
'macro task 2'
'macro task 3'
总结
微任务队列
优先于宏任务队列
执行,微任务队列上创建的宏任务
会被后添加到当前宏任务队列的尾端,微任务队列中创建的微任务会被添加到微任务队列的尾端。只要微任务队列中还有任务
,宏任务队列
就只会等待微任务队列执行完毕后再执行
。
async/await
当我们在函数前使用
async
的时候,使得该函数返回的是一个Promise
对象
async function test() { return 1 // async的函数会在这里帮我们隐士使用Promise.resolve(1) } // 等价于下面的代码 function test() { return new Promise(function(resolve, reject) { resolve(1) }) } # 可见 async 只是一个语法糖,只是帮助我们返回一个Promise而已
await
表示等待,是右侧「表达式」的结果,这个表达式的计算结果可以是 Promise 对象的值或者一个函数的值(换句话说,就是没有特殊限定)。并且只能在带有async
的内部使用.使用
await
时,会从右往左执行,当遇到await
时,会阻塞函数内部处于它后面的代码,去执行该函数外部的同步代码,当外部同步代码执行完毕,再回到该函数内部执行剩余的代码, 并且当await
执行完毕之后,会先处理微任务队列的代码.
async function async1() { console.log( 'async1 start' ) // 2 await async2() // 阻塞后面的代码,去外部执行同步代码 进入new Promise console.log( 'async1 end' ) // 6 } async function async2() { console.log( 'async2' ) // 3 } console.log( 'script start' ) // 1 setTimeout( function () { console.log( 'setTimeout' ) // 8 }, 0 ) async1(); new Promise( function ( resolve ) { console.log( 'promise1' ) // 4 resolve(); } ).then( function () { console.log( 'promise2' ) // 7 } ) console.log( 'script end' ) // 5 # 浏览器运行结果: 'script start' 'async1 start' 'async2' 'promise1' 'script end' 'async1 end' 'promise2' setTimeout
使用事件循环机制分析:
- 首先执行同步代码,
console.log( 'script start' )
- 遇到
setTimeout
,会被推入宏任务队列- 执行
async1()
, 它也是同步的,只是返回值是Promise
,在内部首先执行console.log( 'async1 start' )
- 然后执行
async2()
, 然后会打印console.log( 'async2' )
- 从右到左会执行, 当遇到
await
的时候,阻塞后面的代码,去外部执行同步代码- 进入
new Promise
,打印console.log( 'promise1' )
- 将
.then
放入事件循环的微任务队列- 继续执行,打印
console.log( 'script end' )
- 外部同步代码执行完毕,接着回到
async1()
内部, 由于async2()
其实是返回一个Promise
,await async2()
相当于获取它的值,其实就相当于这段代码Promise.resolve(undefined).then((undefined) => {})
,所以.then
会被推入微任务队列, 所以现在微任务队列会有两个任务。接下来处理微任务队列,打印console.log( 'promise2' )
,后面一个.then
不会有任何打印,但是会执行- 执行后面的代码, 打印
console.log( 'async1 end' )
- 进入第二次事件循环,执行宏任务队列, 打印
console.log( 'setTimeout' )
资料查阅:
本文使用 👉mdnice 排版