portrait

bill-lai

专注 WEB 端开发

JavaScript的宏任务与微任务2022-02-08 11:20

事件队列与事件循环

  我们都知道JavaScript是一门单线程非阻塞的脚本语言,这意味着JavaScript在执行期间都是只有一个主线程来处理所有任务的。而非阻塞是指当代码有异步任务是,主线程会挂起这个异步任务,当这个异步任务执行完毕后,主线程才会在适当时期去执行这个任务的回调。

  当异步任务处理完毕后,JavaScript会将这个任务放置在一个队列中,我们称这个任务为事件队列。这个队列上的上的回调不会立即执行,而是当当前执行栈中的所有任务处理完之后才会去执行事件队列的任务。

  队列是先进先出的线性表。在具体应用中通常用链表或者数组来实现。具体资料参考维基百科

  当前执行栈执行完毕后,JavaScript会去检查当前事件队列是否有任务,如果有则将这个任务添加到当前执行栈中执行这个任务,当任务执行执行完毕后又重复这一操作,构成了一个循环,而这个循环我们就称之为事件循环。整体的流程如下图所示

event-queue

宏任务与微任务

  异步任务分为两种类型,微任务(microtask)微任务(macrotask),不同类型的任务会被分配到不同的事件队列中,执行的时机也会有所不同,为了方便表达我把微任务事件队列称之为微事件队列,宏任务事件队列称之为宏任务队列

  事件循环检查当前事件队列时,首先检查当前微事件队列是否有任务,如果有则添加到当前执行栈执行,当执行完毕后再次检查,直到当前微事件队列为空后,再检查当前宏事件队列是否有任务,如果有则添加到当前执行栈执行。流程如下图所示

event-queue

  首次执行的代码其实也是宏任务,可以这么理解,因为是首次微事件队列是空的所以直接执行宏任务队列中的任务。除了首次加载外,微事件队列中的任务始终是先于宏任务队列任务执行的。

微任务的事件包括以下几种

宏任务的事件包括以下几种

思考

请思考下面几个例子在控制台输出的顺序,最后会贴出答案。

例子1:

// 微任务
const promise = Promise.resolve()
const microtask = (cb) => {
  promise.then(cb)
}

// 宏任务
const macrotask = (cb) => {
  setTimeout(cb)
}

macrotask(() => {
  console.log('macrotask 1')
})

macrotask(() => {
  console.log('macrotask 2')
})

microtask(() => {
  console.log('microtask 1')
})

例子2:

macrotask(() => {
  microtask(() => {
    console.log('microtask 2')
  })
  console.log('macrotask 1')
})

macrotask(() => {
  console.log('macrotask 2')
  microtask(() => {
    console.log('microtask 3')
  })
})

microtask(() => {
  console.log('microtask 1')
})

下面三个例子,我大概模拟了vue2渲染函数的原理:

例子3:

/**
 * html:
 * <div id="output"></div>
 **/

const stack = []
const nextTick = cb => microtask(cb)
const render = fn => {
  if (!stack.includes(fn)) {
    nextTick(() => {
      const index = stack.indexOf(fn)
      if (index > -1) {
        stack.splice(index, 1)
      }
      fn()
    })
    stack.push(fn)
  }
}

const observeItem = (obj, key, val, renderFn) => {
  Object.defineProperty(obj, key, {
    set(newVal) {
      render(renderFn)
      val = newVal
      return true
    },
    get() {
      return val
    }
  })
}

const observe = (obj, renderFn) => {
  for (let key in obj) {
    observeItem(obj, key, obj[key], renderFn)
  }
  renderFn()
}

const data = { count: 0 }
const $container = document.querySelector('#output')

observe(
  data, 
  () => $container.textContent = data.count.toString()
)

nextTick(() => {
  console.log($container.textContent)
})
data.count = 100

例子4:

data.count = 200
nextTick(() => {
  console.log($container.textContent)
})

例子5:

macrotask(() => {
  console.log($container.textContent)
})
data.count = 300

















答案

答案在下方,这里就不再解释了,记住除了首次执行外微任务始终于宏任务前执行,>事件队列始终是先进先出,宏事件队列与微任务队列谁先加入谁先执行,那么这些题目就难不倒你了。

// 例子1
// "microtask 1"
// "macrotask 1"
// "macrotask 2"

// 例子2
// "microtask 1"
// "macrotask 1"
// "microtask 2"
// "macrotask 2"
// "microtask 3"

// 例子3
// "0"

// 例子4
// "200"

// 例子5
// "300"

©2021 - bill-lai 的小站 -站点源码

本站使用ReactHookTypeScriptgithubAPISimpleMDE制作