Apr 07, 2020

JS 事件循环;宏任务,微任务,setTimeout


JS 事件循环

  • JS 是单线程的语言,JS 里面的任务要一个一个执行。为了提高运行速度,JS 将任务分为 同步异步 两类,但本质上异步是 JS 用同步的方法模拟的
  • js 事件循环机制,决定了代码执行顺序
  • 在单次的事件循环中,JS 执行某个宏任务后会检查是否有微任务存在,如果有则处理完微任务,再开启下一个宏任务。如果没有下一个宏任务,则开启下一次事件循环

deba5QqYSVwguDn.png

表述:

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入 Event Table 并注册函数
  • 当指定的事情完成时,Event Table 会将这个函数移入 Event Queue
  • 线程内的任务执行完毕为空,会去 Event Queue 读取对应的函数,进入主线程执行
  • 上述过程会不断重复,也就是常说的 Event Loop

举例:

let data = []; $.ajax({ url:www.javascript.com, data:data, success:() => { console.log('发送成功!'); } }) console.log('代码执行结束');
  1. ajax 请求进入 Event Table
  2. 主线程执行 console.log()
  3. ajax 执行完毕后,success 方法进入 Event Queue
  4. 主线程从 Event Queue 读取回调函数 success 并执行

宏任务,微任务,setTimeout

  • 宏任务与微任务

    • macro-task (宏任务):包括整体代码 scriptsetTimeoutsetInterval
    • micro-task (微任务):Promise.thenprocess.nextTick
    • 具体是宏任务还是微任务,与浏览器、运行环境都有关系
  • 在一个 Event Loop 中,微任务未执行完毕前,不会执行下一个宏任务

  • 宏任务和微任务执行完成后都会判断是否还有微任务,有的话执行微任务,没有就执行宏任务,如此循坏

  • setTimeout 作为一个宏任务,延迟参数设为 0 时,只能表示尽快执行。一般浏览器中其 delay 时间最短为 4ms。详细信息见 MDN:

    bookmark

举例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
  • 最后再次检查微任务队列,没有啦。再检查宏任务队列,也没啦
  • 进入下一个事件循环

例子来源:

bookmark

JOJO 的奇妙比喻

银行上班时,为了控制人员密度,将门口排队的人放进银行,关上门,这是一轮事件循环。

进入银行的人去取号,等待柜员叫号,这个相当于浏览器依次执行宏任务

有些人进入大厅后没有直接取号,而是先去找了大堂经理,相当于异步任务,进入 Event Table。问完了大堂经理,知道自己该取什么类型的号了,再去取号,相当于执行完毕,进入 Event Queue,等待叫号。

叫到号之后去办理业务相当于主线程处理宏任务。宏任务处理完毕后,柜员会询问是否还有其他需要办理的业务,这些任务无需重新取号,如果需要办理则相等于添加微任务,此时会优先执行微任务。

只有当一个人的所有业务办理完毕后,才会叫下一个人的号,相当于执行下一个宏任务

宏任务依次执行直至大厅里所有人的业务全部办理完毕,这时银行会打开一次大门放进排队的人,开启下一次事件循环

关联阅读

bookmark

bookmark