JS 事件循环
- JS 是单线程的语言,JS 里面的任务要一个一个执行。为了提高运行速度,JS 将任务分为
同步
和异步
两类,但本质上异步是 JS 用同步的方法模拟的 - js 事件循环机制,决定了代码执行顺序
- 在单次的事件循环中,JS 执行某个宏任务后会检查是否有微任务存在,如果有则处理完微任务,再开启下一个宏任务。如果没有下一个宏任务,则开启下一次事件循环
表述:
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入 Event Table 并注册函数
- 当指定的事情完成时,Event Table 会将这个函数移入 Event Queue
- 线程内的任务执行完毕为空,会去 Event Queue 读取对应的函数,进入主线程执行
- 上述过程会不断重复,也就是常说的 Event Loop
举例:
let data = []; $.ajax({ url:www.javascript.com, data:data, success:() => { console.log('发送成功!'); } }) console.log('代码执行结束');
- ajax 请求进入 Event Table
- 主线程执行
console.log()
- ajax 执行完毕后,
success
方法进入 Event Queue - 主线程从 Event Queue 读取回调函数
success
并执行
宏任务,微任务,setTimeout
-
宏任务与微任务
- macro-task (宏任务):包括整体代码
script
,setTimeout
,setInterval
等 - micro-task (微任务):
Promise.then
,process.nextTick
等 - 具体是宏任务还是微任务,与浏览器、运行环境都有关系
- macro-task (宏任务):包括整体代码
-
在一个 Event Loop 中,微任务未执行完毕前,不会执行下一个宏任务
-
宏任务和微任务执行完成后都会判断是否还有微任务,有的话执行微任务,没有就执行宏任务,如此循坏
-
setTimeout 作为一个宏任务,延迟参数设为 0 时,只能表示尽快执行。一般浏览器中其 delay 时间最短为
4ms
。详细信息见 MDN:
举例1:
setTimeout(() => console.log('setTimeout-1'), 0) async function todo1 (params) { console.log('todo1-await-above') await Promise.resolve(99) console.log('todo1-await-under') } todo1() new Promise((resolve, reject) => { console.log('promise-1') resolve() }).then(data => { console.log('promise-then-1') }) console.log('end') // todo1-await-above // promise-1 // end // promise-then-1 // todo1-await-under // setTimeout-1
解析:
- 开启一个事件循环
- 这段代码作为宏任务,进入主线程
- 先遇到
setTimeout
, 等待4ms
后,将其回调函数注入到宏任务Event Queue
- 接下来遇到
todo1
函数,没调用,就当看不到 - 调用
todo1
函数 - 遇到
console.log('todo1-await-above')
立即执行并输出 - 遇到
await promise
,相关于Promise.then
,将等待promise
执行结束后再继续执行,这里将执行权交给todo1
函数外部继续执行 - 遇到
new Promise
立即执行console.log('promise-1')
并输出,之后执行resolve()
,接着将then
的回调函数注入到微任务Event Queue
- 遇到
console.log('end')
,立即执行并输出 - 注意代码还有
console.log('todo1-await-under')
没有执行,在这里执行并放到微任务Event Queue
【作者译:因为await
后面跟着状态不确定的promise
】 - 好了,整体代码
<script>
作为第一轮的宏任务执行结束,接下来按照先进先出原则,执行微任务队列事件。 - 执行并输出
promise-then-1
- 执行并输出
todo1-await-under
- 检查宏任务队列,这时还有
setTimeout
回调函数需要执行 - 执行并输出
setTimeout-1
- 最后再次检查微任务队列,没有啦。再检查宏任务队列,也没啦
- 进入下一个事件循环
举例2:
setTimeout(() => console.log('setTimeout-1')) async function todo1 (params) { console.log('todo1-await-above') // await Promise.resolve(99) await 123 console.log('todo1-await-under') } todo1() new Promise((resolve, reject) => { console.log('promise-1') resolve() }).then(data => { console.log('promise-then-1') }) console.log('end') // todo1-await-above // promise-1 // end // todo1-await-under // promise-then-1 // setTimeout-1
解析:
- 开启一个事件循环
- 老规矩,这段代码作为宏任务,进入主线程
- 先遇到
setTimeout
, 等待4ms
后,将其回调函数注入到宏任务Event Queue
- 接下来遇到
todo1
函数,没调用,就当看不到 - 调用
todo1
函数 - 遇到
console.log('todo1-await-above')
立即执行并输出 - 遇到
await 123
因为这里 await 一个具体值,状态是明确的,所以继续向下执行,将console.log('todo1-await-under')
放到微任务 Event Queue - 遇到
new Promise
立即执行console.log('promise-1')
并输出,之后执行resolve()
,将then
的回调函数注入到微任务Event Queue
- 遇到
console.log('end')
,立即执行并输出 - 好了,整体代码
<script>
作为第一轮的宏任务执行结束,接下来按照先进先出原则,先执行微任务队列事件。 - 执行并输出
todo1-await-under
- 执行并输出
promise-then-1
- 检查宏任务队列,这时还有
setTimeout
回调函数需要执行 - 执行并输出
setTimeout-1
- 最后再次检查微任务队列,没有啦。再检查宏任务队列,也没啦
- 进入下一个事件循环
例子来源:
JOJO 的奇妙比喻
银行上班时,为了控制人员密度,将门口排队的人放进银行,关上门,这是一轮事件循环。
进入银行的人去取号,等待柜员叫号,这个相当于浏览器依次执行宏任务。
有些人进入大厅后没有直接取号,而是先去找了大堂经理,相当于异步任务,进入 Event Table。问完了大堂经理,知道自己该取什么类型的号了,再去取号,相当于执行完毕,进入 Event Queue,等待叫号。
叫到号之后去办理业务相当于主线程处理宏任务。宏任务处理完毕后,柜员会询问是否还有其他需要办理的业务,这些任务无需重新取号,如果需要办理则相等于添加微任务,此时会优先执行微任务。
只有当一个人的所有业务办理完毕后,才会叫下一个人的号,相当于执行下一个宏任务。
宏任务依次执行直至大厅里所有人的业务全部办理完毕,这时银行会打开一次大门放进排队的人,开启下一次事件循环。