Skip to content

任务轮询/任务管理 Event Loop

js 是一个单线程语言,也就是同一时间只能执行一个任务。为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程阻塞,浏览器会维护一个任务队列,当执行栈中的任务完成后,主线程会读取任务队列,取出排在第一位的一个任务,执行完毕后,再读取任务队列,取出排在第二位的一个任务,以此类推。

  • 主线程内的任务执行完成后,才会执行任务队列中的其他任务
  • 有新任务到来,主线程会先执行任务队列中的任务,然后再执行主线程任务

任务包括:script、setTimeout、setInterval、UI 交互事件、UI 渲染、Promise、XMLHttpRequest 等

宏任务和微任务

执行过程

  • 同步代码

    先执行同步代码

  • 异步代码

    放置于任务队列中,等待同步代码执行完成后执行

    • 宏任务
      • setTimeout
      • setInterval
      • DOM 渲染
    • 微任务
      • Promise

    先执行微任务队列再执行宏任务队列

脚本加载

引擎执行任务时不会进行 DOM 渲染,所以如果把 script 标签定义在前面,要执行完成后再渲染 DOM,所以应该将 script 标签定义在 body 标签结束前

定时器

定时器会经过定时器模块处理后放入到异步任务队列,需要等待同步任务执行完成后才执行

微任务

微任务一般有用户代码产生,微任务优先级要高于宏任务。Promise.then 是典型的微任务,实例化 Promise 时执行的代码是同步的,.then注册的回调函数是异步微任务。

任务执行顺序是同步任务、微任务、宏任务。

示例:下面的这些会如何执行呢?

js
setTimeout(() => {
  console.log('setTimeout')
})

new Promise((resolve, reject) => {
  console.log('hello promise') // 同步代码
  resolve()
}).then(() => {
  console.log('then')
})
console.log('hello world') // 同步代码
  1. 执行到 setTimeout 宏任务,将其放置到宏任务队列,等待执行
  2. 执行同步代码,输出hello promise
  3. 执行到 Promise.then(() => { console.log('then') }) 微任务,将其放置到微任务队列,等待执行
  4. 执行到主代码输出 hello world
  5. 主线任务执行完毕,通过事件循环遍历微任务队列,将 Promise.then(() => { console.log('then') })微任务放入主线程执行,输出then
  6. 主线程执行完毕,通过事件循环遍历宏任务队列,将 setTimeout 宏任务放入主线程执行,输出setTimeout
js
setTimeout(() => {
  console.log('setTimeout')
  new Promise((resolve) => {
    console.log('setTimeout promise')
    resolve()
  }).then(() => {
    console.log('setTimeout promise then')
  })
})

new Promise((resolve, reject) => {
  console.log('hello promise')
  resolve()
}).then(() => {
  console.log('then')
})
console.log('hello world')
  1. 执行到 setTimeout 宏任务,将其放置到宏任务队列,等待执行
  2. 执行到主线程代码输出 hello promise
  3. 执行到 Promise.then(() => { console.log('then') }) 微任务,将其放置到微任务队列,等待执行
  4. 执行到主代码输出 hello world
  5. 主线程任务执行完成,通过事件循环遍历微任务队列,将 Promise.then(() => { console.log('then') }) 微任务放入主线程执行,输出then
  6. 主线程执行完毕,通过事件循环遍历宏任务队列,将 setTimeout 宏任务放入主线程执行,输出setTimeout setTimeout promise,执行时发现 Promise.then(() => { console.log('setTimeout promise then') }) 微任务,将其放于微任务队列中
  7. 主线程任务执行完成,通过事件循环遍历微任务队列,将 Promise.then(() => { console.log('setTimeout promise then') }) 微任务放置于主线程中执行,输出setTimeout promise then
  8. 任务执行完毕
js
let i = 0
setTimeout(() => {
  console.log(++i)
})
setTimeout(() => {
  console.log(++i)
})
console.log(i)
// 输出结果
// 0
// 1
// 2
  1. 执行到 setTimeout 宏任务,将其放置到宏任务队列,等待执行
  2. 执行到主线程代码输出 0
  3. 执行到 setTimeout(() => { console.log(++i) }) 宏任务,将其放置到宏任务队列,等待执行
  4. 执行到 `setTimeout(() => { console.log(++i) })
## 问题:为什么 setTimeout 和 setInterval 的回调函数执行时间会不准确?

定时器会放置于定时器模块中,在计时结束后,会将回调函数放置于宏任务队列中,等待同步代码执行完成后执行。如果同步代码耗时比较长,就会导致回调函数执行时间不准确。