JS的事件循环机制
1. 浏览器的事件循环机制
1.1 事件执行的顺序
-
当事件开始时,首先会进入
JS
主线程机制,由于JS
属于单线程机制,因此存在多个任务的时候会存在等待的情况,先等待最先进入线程的事件处理完毕 -
这样就会出现等待的情况,如果之前的事件没有执行完成,后面的事件就会一直等待
-
但是类似于
AJAX
和setTimeout
,setInterval
等待的事件,就出现了异步处理 -
通过将异步的事件交给异步模块处理,主线程就会去并行的处理后面的事件
-
当主线程空闲的时候,异步处理完成,主线程就会读取异步处理返回的
callback
执行异步事件后续的操作
同步执行就是主线程按照事件的顺序,依次执行事件
异步执行就是主线程先跳过等待,执行后面的事件,异步事件交给异步模块处理
图解
- 事件开始,进入主线程
- 主线程如果发现异步事件,将异步事件移交给异步模块,主线程继续执行后面的同步事件
- 当异步进程执行完毕之后,再回到事件队列中
- 主线程执行完毕,就会查询事件队列,如果事件队列中存在事件,事件队列就将事件推到主线程
- 主线程执行事件队列推过来的事件,执行完毕后再去事件队列中查询… 这个循环的过程就是事件循环
1.2 异步任务又分为宏任务(macrotask)和微任务(microtask)
常见的宏任务:setTimeout setInterval I/O script
常见的微任务:promise
同一事件循环中,微任务永远在宏任务之前
同一事件循环中,微任务永远在宏任务之前
同一事件循环中,微任务永远在宏任务之前
小栗子1
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
Promise.resolve().then(()=> {
console.log(3)
})
console.log(4)
// 结果 1 4 3 2
-
任务开始执行,进入执行栈。遇到
console.log(1)
,是同步任务,输出1
; -
执行栈继续执行,遇到定时器
setTimeout
,是异步宏任务,进入异步进程的宏任务队列; -
执行栈继续执行,遇到
Promise
,是异步微任务,进入异步进程的微任务队列; -
执行栈继续执行,遇到
console.log(4)
,是同步任务,输出4
; -
至此同步任务执行完成,开始查询任务队列,微任务队列在宏任务队列之前,先执行微任务队列输出
3
; -
微任务队列执行完毕,再查询宏任务队列,输出
2
,至此整个任务队列完成,最后输出1 4 3 2
小栗子2
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
new Promise((resolve) => {
console.log(3)
resolve(4)
}).then((res)=> {
console.log(res)
})
console.log(5)
// 最后结果 1 3 5 4 2
大栗子3
setTimeout(() => console.log('setTimeout1'), 0) //1宏任务
setTimeout(() => { //2宏任务
console.log('setTimeout2')
Promise.resolve().then(() => {
console.log('promise3')
Promise.resolve().then(() => {
console.log('promise4')
})
console.log(5)
})
setTimeout(() => console.log('setTimeout4'), 0) //4宏任务
}, 0)
setTimeout(() => console.log('setTimeout3'), 0) //3宏任务
Promise.resolve().then(() => {//1微任务
console.log('promise1')
})
// 最后结果
/**
promise1
setTimeout1
setTimeout2
promise3
5
promise4
setTimeout3
setTimeout4
*/
1.3 遇到async
和await
的情况
一旦遇到await
就立刻让出线程,阻塞后面的代码,先执行async
外面的同步代码
等候之后,对于await
来说分两种情况:不是promise
对象;是promise
对象
- 如果不是
promise
,await
会阻塞后面的代码,先执行async
外面的同步代码,同步代码执行完毕后,在回到async
内部,把promise
的东西,作为await
表达式的结果 - 如果它等到的是一个
promise
对象,await
也会暂停async
后面的代码,先执行async
外面的同步代码,等着Promise
对象fulfilled
,然后把resolve
的参数作为await
表达式的运算结果。 - 如果一个
Promise
被传递给一个await
操作符,await
将等待Promise
正常处理完成并返回其处理结果。
----栗子(没有 async
await
)代码依次执行
function fn1() {
return 1
}
function fn2 () {
console.log(2)
console.log(fn1())
console.log(3)
}
fn2()
console.log(4)
// 结果 2 1 3 4
----栗子(没有promise
)如果不是promise
,await
会阻塞后面的代码,先执行async
外面的同步代码,同步代码执行完毕后,在回到async
内部,把promise
的东西,作为await
表达式的结果
async function fn1() {
return 1
}
async function fn2 () {
console.log(2)
console.log(await fn1())
console.log(3)
}
fn2()
console.log(4)
// 结果 2 4 1 3
—栗子(存在promise
) 如果它等到的是一个 promise
对象,await
也会暂停async
后面的代码,先执行async
外面的同步代码,等着 Promise
对象 fulfilled
,然后把 resolve
的参数作为 await
表达式的运算结果。
function fn1 () {
return new Promise((reslove) => {
reslove(1)
})
}
async function fn2() {
console.log(2)
console.log(await fn1())
console.log(3)
}
fn2()
console.log(4)
// 结果 2 4 1 3
2. NodeJS
的事件循环
NodeJS
相对于浏览器的事件循环多了一个微任务process.nextTick()
和宏任务setImmediate()
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
-
timers
阶段: 这个阶段执行setTimeout(callback)
和setInterval(callback)
预定的 callback; -
I/O callbacks
阶段: 此阶段执行某些系统操作的回调,例如TCP错误的类型。 例如,如果TCP套接字在尝试连接时收到 ECONNREFUSED,则某些* nix系统希望等待报告错误。 这将操作将等待在I/O回调阶段执行; -
idle, prepare
阶段: 仅node内部使用; -
poll
阶段: 获取新的I/O事件, 例如操作读取文件等等,适当的条件下node将阻塞在这里; -
check
阶段: 执行setImmediate()
设定的callbacks; -
close callbacks
阶段: 比如socket.on(‘close’, callback)
的callback会在这个阶段执行;
process.nextTick()
process.nextTick(() => {
//做些事情
})
每当事件循环进行一次完整的行程时,我们都将其称为一个滴答。
当将一个函数传给 process.nextTick()
时,则指示引擎在当前操作结束(在下一个事件循环滴答开始之前)时调用此函数
传给 process.nextTick()
的函数会在事件循环的当前迭代中(当前操作结束之后)被执行。 这意味着它会始终在 setTimeout
和 setImmediate
之前执行。
相比于Promise
,process.nextTick()
会先执行
Promise.resolve().then(() => {
console.log('promise')
})
process.nextTick(()=> {
console.log('process')
})
// 运行结果
/*
process
promise
*/
setImmediate()
setImmediate(() => {
//运行一些东西
})
作为 setImmediate() 参数传入的任何函数都是在事件循环的下一个迭代中执行的回调
延迟 0 毫秒的 setTimeout()
回调与 setImmediate()
非常相似。 执行顺序取决于各种因素,但是它们都会在事件循环的下一个迭代中运行。