Skip to content
大纲

EventLoop

js 线程的特点

  • js 的特点是单线程的,js 主要用途是用户交互,如果是多线程操作就容易产生冲突,那么单线程就意味着所有任务都需要排队,如果前一个任务耗时很长,后面的任务就不得不一直等着,所以就出现了同步、异步任务

同步和异步任务在 js 中是如何执行的

  • js 代码运行会有一个主线程和一个任务队列,主线程会自上而下的依次执行 js 代码,形成一个执行栈
  • 同步任务会被放到主线程中依次执行,而异步任务被放到任务队列中执行,执行完会在任务队列中打一个标记,形成一个对应的事件。promise 是何时被放入异步任务队列的?是 运行时遇到所有 then 或 catch,就直接放进任务队列了吗?
  • 主线程任务执行完毕,会从任务队列中提取对应的事件? 谁负责提取?
  • EventLoop 是主线程重复从事件队列中取消息、执行的过程;事件队列遵循 FIFO 的原则

任务队列可以有多个,分为 macro-task(由宿主发起) 和 micro-task(由 js 自身发起)

  • macro-task 包括:script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI rendering、事件队列(未确定)、postMessage、MessageChannel
  • micro-task 包括:process.nextTick、 Promise.then() 或 catch()、 Object.observe(已废弃)、 MutationObserver(html5 新特性)、 V8 的垃圾回收过程、Node 独有的 process.nextTick()、queueMicrotask(Chrome 官方 API,可节省实例化 Promise 的开销)
  • 其他:requestAnimationFrame(有争议)、requestIdleCallback、MessageChannel
  • requestAnimationFrame(有争议)、requestIdleCallback:是和宏任务性质一样的任务,但其既不是宏任务也不是微任务
  • requestIdleCallback 是在浏览器渲染后有空闲时间时执行,如果 requestIdleCallback 设置了第二个参数 timeout,则会在超时后的下一帧强制执行
  • 事件运行机制
    • 执行一个宏任务(栈中没有就从事件队列中获取),执行过程中如果遇到微任务,就将它添加到微任务的任务队列中;
    • 宏任务执行完毕后,立即执行当前微任务队列的所有微任务;
    • 当前微任务执行完毕,开始检查渲染,如果需要渲染则 GUI 线程接管渲染;
      • 触发 resize、scroll 事件,建立媒体查询(执行一个任务中如果生成了微任务,则执行完任务该后就会执行所有的微任务,然后再执行下一个任务)。
      • 建立 css 动画(执行一个任务中如果生成了微任务,则执行完该任务后就会执行所有的微任务,然后再执行下一个任务)。
      • 执行 requestAnimationFrame 回调(执行一个任务中如果生成了微任务,则执行完该任务后就会执行所有的微任务,然后再执行下一个任务)。
      • 执行 IntersectionObserver 回调(执行一个任务中如果生成了微任务,则执行完该任务后就会执行所有的微任务,然后再执行下一个任务)。
    • 更新渲染屏幕
    • 浏览器判断当前帧是否还有空闲时间,如果有空闲时间,从 requestIdleCallback 回调函数队列中取第一个,执行它。执行微任务队列里的所有微任务,直到 requestIdleCallback 回调函数队列清空或当前帧没有空闲时间
    • 渲染完毕后,JS 线程继续接管,当前微任务队列的所有 Web Worker 任务,则执行
    • 开始下一个宏任务。 注:
  1. requestAnimationFrame 和 requestIdleCallback 是和宏任务性质一样的任务,只是他们的执行时机不同而已
  2. 浏览器在每一轮 Event Loop 事件循环中不一定会去重新渲染屏幕,会根据浏览器刷新率以及页面性能或是否后台运行等因素判断的,浏览器的每一帧是比较固定的,会尽量保持 60Hz 的刷新率运行,每一帧中间可能会进行多轮事件循环
  3. requestAnimationFrame 是与浏览器是否渲染相关联的。它是在浏览器渲染前,在微任务执行后执行。
  4. requestIdleCallback 是在浏览器渲染后有空闲时间时执行,如果 requestIdleCallback 设置了第二个参数 timeout,则会在超时后的下一帧强制执行

不同宏任务与微任务队列之间的优先级

  1. 先执行 macrotasks:I/O -> UI 渲染-> requestAnimationFrame
  2. 再执行 microtasks :process.nextTick -> Promise -> MutationObserver ->Object.observe
  3. 再把 setTimeout、setInterval、setImmediate【三个货不讨喜】 塞入一个新的 macrotasks, 依次:setTimeout-> setInterval -> setImmediate

具体代码执行分析

js
async function async1() {
  console.log('async1 start') //(2)
  await async2()
  console.log('async1 end') //(6)
}
async function async2() {
  console.log('async2') //(3)
}
console.log('script start') //(1)
setTimeout(function () {
  console.log('settimeout') //(8)
}, 0)
async1()
new Promise(function (resolve) {
  console.log('promise1') //(4)
  resolve()
}).then(function () {
  console.log('promise2') //(7)
})
console.log('script end') //(5)
  • 首先,事件循环从宏任务队列开始,读取整体代码,遇到相应的任务,会分发到对应任务队列中去
  • 我们看到定义了两个 async 函数,没有调用,继续遇到了 console 语句,则执行输出,遇到 setTimeout 将其分发到对应的任务队列中
  • 继续执行了 async1()函数,其中 await 之前的代码是立即执行的,遇到 await,会将其表达式执行一遍,即执行了 async2()函数,输出 'async2',紧接着把 await 后面的代码console.log('async1 end')加入到 microtask 中的 Promise 队列中,接着跳出 async1()函数,执行后面的代码。
  • script 继续执行,遇到了 Promise 实例,立即执行其构造函数,后面的.then则被分发到 microtask 的 Promise 队列中,所以会先输出 promise1,然后执行 resolve,将 promise2 分配到对应队列。
  • script 任务继续往下执行,输出了 script end,至此,全局任务就执行完毕了。
  • 宏任务完毕后,开始清空微任务队列,微任务的Promise队列中有两个任务,即async1 endpromise2,按照先进先出的原则进行输出,微任务执行完毕,检查渲染,交给 GUI 线程
  • 开始第二轮的宏任务
js
function asyncGet(x) {
  return new Promise((resolve) =>
    // 将宏任务添加到宏任务列表
    setTimeout(() => {
      // 执行输出,并改变 Promise 状态
      console.log('a')
      resolve(x)
    }, 500)
  )
}

async function test() {
  console.log('b') //(2)
  const x = 3 + 5
  console.log(x) //(3)

  const a = await asyncGet(1) // 执行添加宏任务,等待,待状态改变再执行后续代码
  console.log(a) //resolve(1)

  const b = await asyncGet(2) // 执行添加宏任务,等待,待状态改变再执行后续代码
  console.log(b) //resolve(2)

  // 异步版本
  // const [a, b] = await Promise.all([
  //   asyncGet(1),
  //   asyncGet(2)
  // ])

  console.log('c') // 输出
  return a + b
}

const now = Date.now()
console.log('d') //(1)
test().then((x) => {
  console.log(x) // 收到返回结果 a + b的值,输出
  console.log(`elapsed: ${Date.now() - now}`) // 输出
})
console.log('f') // (4)
js
// 返回一个Promise对象的数组,并不是我们期待的value数组
// await只会暂停map的callback,因此map完成时,不能保证asyncGet也全部完成
async function getAll(vals) {
  return vals.map(async (v) => await asyncGet(v))
}

执行过程详解

js
const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  }, 1000)
})
const promise2 = promise1.then(() => {
  throw new Error('error!!!')
})
console.log('promise1', promise1)
console.log('promise2', promise2)
setTimeout(() => {
  console.log('promise1', promise1)
  console.log('promise2', promise2)
}, 2000)
  • 先执行第一个 new Promise 中的函数,碰到 setTimeout 将它加入下一个宏任务列表
  • 跳出 new Promise,碰到 promise1.then 这个微任务,但其状态还是为 pending,这里理解为先不执行
  • promise2 是一个新的状态为 pending 的 Promise
  • 执行同步代码 打印 promise1 的状态为 pending 与 promise2 的状态为 pending
  • 碰到第二个定时器,将其放入下一个宏任务列表
  • 第一轮宏任务执行结束,并且没有微任务需要执行,因此执行第二轮宏任务
  • 先执行第一个定时器里的内容,将 promise1 的状态改为 resolved 且保存结果并将之前的 promise1.then 推入微任务队列
  • 该定时器中没有其它的同步代码可执行,因此执行本轮的微任务队列,也就是 promise1.then,它抛出了一个错误,且将 promise2 的状态设置为了 rejected
  • 第一个定时器执行完毕,开始执行第二个定时器中的内容,打印 promise1 的状态为 resolved,打印 promise2 的状态为 rejected

async

  • 执行到 await fn() 语句时,会阻塞 fn() 后面代码的执行,因此会先去执行 fn() 中的同步代码后,跳出当前函数,继续执行其他代码,只有 fn() Promise 被 fulfill 或者 reject,再继续执行之后的代码。可以理解为「紧跟着 await 后面的语句相当于放到了 new Promise 中,下一行及之后的语句相当于放在 Promise.then 中」
js
async function async1() {
  console.log('async1 start')
  // 但 await后面的Promise是没有返回值的,也就是它的状态始终是pending状态,因此相当于一直在await,
  await new Promise((resolve) => {
    console.log('promise1')
  })
  console.log('async1 success')
  return 'async1 end'
}
console.log('srcipt start')
async1().then((res) => console.log(res))
console.log('srcipt end')
  • 写出执行结果
js
async function testSometing() {
  console.log('执行testSometing')
  return 'testSometing'
}

async function testAsync() {
  console.log('执行testAsync')
  return Promise.resolve('hello async')
}

async function test() {
  console.log('test start...')
  const v1 = await testSometing()
  console.log(v1)
  const v2 = await testAsync()
  console.log(v2)
  console.log(v1, v2)
}

test()

var promise = new Promise((resolve) => {
  console.log('promise start...')
  resolve('promise')
})
promise.then((val) => console.log(val))

console.log('test end...')
js
const async1 = async () => {
  console.log('async1')
  setTimeout(() => {
    console.log('timer1')
  }, 2000)
  // await的new Promise要是没有返回值的话则不执行后面的内容
  await new Promise((resolve) => {
    console.log('promise1')
  })
  console.log('async1 end')
  return 'async1 success'
}
console.log('script start')
async1().then((res) => console.log(res))
console.log('script end')
Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .catch(4)
  .then((res) => console.log(res))
setTimeout(() => {
  console.log('timer2')
}, 1000)