浅谈JS运行机制,宏任务,微任务

1,241 阅读3分钟

JS 单线程

先聊聊JS的单线程工作,单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

那怎么样解决这种问题呢,在浏览器平台下,浏览器底层开启了多线程去执行了一些任务,形成了我们常说的异步任务,比如说定时器setTimeout,异步任务可以很好的解决代码阻塞问题,但这也会带来代码的执行顺序没有像同步任务那样直观明了,这也是我们本文的重点。

JS 运行机制

一开始整个脚本作为一个宏任务执行。执行过程中同步代码直接执行,宏任务等待时间到达或者成功后,将方法的回调放入宏任务队列中,微任务进入微任务队列。

当前主线程的宏任务执行完出队,检查并清空微任务队列。接着执行浏览器 UI 线程的渲染工作,检查web worker 任务,有则执行。

然后再取出一个宏任务执行。以此循环...

EventLoop 是一种循环机制 ,不断去轮询一些队列 ,从中找到需要执行的任务并按顺序执行的一个执行模型。

宏任务与微任务

宏任务可以理解为每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

浏览器为了让 JS 内部宏任务 与 DOM 操作能够有序的执行,会在一个宏任务执行结束后,在下一个宏任务执行开始前,对页面进行重新渲染。

宏任务包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、MessageChannel 等。

微任务可以理解是在当前任务执行结束后需要立即执行的任务。也就是说,在当前任务后,在渲染之前,执行清空微任务。

所以它的响应速度相比宏任务会更快,因为无需等待 UI 渲染。

微任务包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)等。

代码示例

console.log('start')
setTimeout(() => {
  console.log('s1')
  Promise.resolve().then(() => {
    console.log('p2')
  })
  Promise.resolve().then(() => {
    console.log('p3')
  })
})

Promise.resolve().then(() => {
  console.log('p1')
  setTimeout(() => {
    console.log('s2')
  })
  setTimeout(() => {
    console.log('s3')
  })
})
console.log('end')

代码分析EventLoop

  1. 首先遇到 console.log('start') ,输出 start,继续执行。
  2. 遇到 setTimeout,由于 setTimeout 是宏任务,时间到了将其放入宏任务队列中,继续执行。
  3. 遇到 Promise.then ,由于当前是微任务,将其放入微任务队列中,继续执行。
  4. 遇到 console.log('end') ,输出 end
  5. 此时,当前主线程中的任务已全部执行完毕, EventLoop 就开始工作,找需要执行的任务压入到执行栈中,微任务优先于宏任务,因此先把 Promise.resolve().then 压入到执行栈开始继续执行。
  6. 遇到 console.log('p1') ,输出 p1 ,继续执行。
  7. 遇到两个 setTimeout s2s3, 放入到宏任务队列中等待执行。
  8. 此时,当前主线程中的任务又全部执行完毕,检查并清空微任务队列,由于此时没有微任务,然后就取出之前第2步骤的宏任务 setTimeout 开始执行。
  9. 输出 s1 ,紧接着又遇到 p2p3两个微任务,将其放入微任务队列中。
  10. 此时,当前主线程中的任务又全部执行完毕,检查并清空微任务队列。所以相继输出 p2p3
  11. 此时,当前主线程中的任务又全部执行完毕,检查并清空微任务队列,由于此时没有微任务,然后就取出之前第7步骤的宏任务 setTimeout 开始执行。所以相继输出 s2s3
  12. 整个js代码已全部完毕,最终输出顺序 startendp1s1p2p3s2s3