EventLoop
- 什么是事件循环?
- 什么是消息队列?
- 宏任务和微任务
- 哪些属于宏任务?
- 哪些属于微任务?
- 事件循环,消息队列与宏任务、微任务之间的关系是什么?
- 微任务添加和执行流程示意图
Event Loop
- 初始状态下,调用栈空。微任务队列空,宏任务队列里有且只有一个 script 脚本(整体代码)。这时首先执行并出队的就是 整体代码
- 整体代码作为宏任务进入调用栈,进行同步任务和异步任务的区分
- 同步任务直接执行并且在执行完之后出栈,异步任务进行微任务与宏任务的划分,分别被推入进入微任务队列和宏任务队列
- 等同步任务执行完了(调用栈为空)以后,再处理微任务队列,将微任务队列压入调用栈
- 当调用栈中的微任务队列被处理完了(调用栈为空)之后,再将宏任务队列压入调用栈,直至调用栈再一次为空,一次轮回结束
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 UI 渲染
- 事件队列(未确定)
- postMessage
- MessageChannel(异步的宏任务)
- micro-task 包括:
- Promise(then 或 catch 等)
- Object.observe(已废弃)
- MutationObserver(html5 新特性)
- process.nextTick()(Node 独有)
- queueMicrotask(Chrome 官方 API,可节省实例化 Promise 的开销)
- V8 的垃圾回收过程
- 其他:
- requestAnimationFrame(有争议)
- requestIdleCallback
说明
- requestAnimationFrame(有争议)、requestIdleCallback:是和宏任务性质一样的任务,但其既不是宏任务也不是微任务
- requestIdleCallback 是在浏览器渲染后有空闲时间时执行,如果 requestIdleCallback 设置了第二个参数 timeout,则会在超时后的下一帧强制执行
我们看下浏览器里的一帧发生了什么
通常情况下,浏览器的一帧为 16.7ms。由于 js 是单线程,那么它内部的一些事件,比如 click 事件,宏任务,微任务,requestAnimatinFrame,requestIdleCallback 等等都会在浏览器帧里按一定的顺序去执行。具体的执行顺序如下:
浏览器一帧里回调的执行顺序为:
- 用户事件:最先执行,比如 click 等事件。
- js 代码:宏任务和微任务,这段时间里可以执行多个宏任务,但是必须把微任务队列执行完成。宏任务会被浏览器自动调控。比如浏览器如果觉得宏任务执行时间太久,它会将下一个宏任务分配到下一帧中,避免掉帧。
- 在渲染前执行 scroll/resize 等事件回调。
- 在渲染前执行 requestAnimationFrame 回调。
- 渲染界面:面试中经常提到的浏览器渲染时 html、css 的计算布局绘制等都是在这里完成。
- requestIdleCallback 执行回调:如果前面的那些任务执行完成了,一帧还剩余时间,那么会调用该函数。
事件循环,消息队列与宏任务、微任务之间的关系是什么?
- 宏任务入队消息队列,可以将消息队列理解为宏任务队列
- 每个宏任务内有一个微任务队列,执行过程中微任务入队当前宏任务的微任务队列
- 宏任务微任务队列为空时才会执行下一个宏任务
- 事件循环捕获队列出队的宏任务和微任务并执行
事件循环会不断地处理消息队列出队的任务,而宏任务指的就是入队到消息队列中的任务,每个宏任务都有一个微任务队列,宏任务在执行过程中,如果此时产生微任务,那么会将产生的微任务入队到当前的微任务队列中,在当前宏任务的主要任务完成后,会依次出队并执行微任务队列中的任务,直到当前微任务队列为空才会进行下一个宏任务。
事件运行机制
- 执行一个宏任务(栈中没有就从事件队列中获取),执行过程中如果遇到微任务,就将它添加到微任务的任务队列中;
- 宏任务执行完毕后,立即执行当前微任务队列的所有微任务;
- 当前微任务执行完毕,开始检查渲染,如果需要渲染则 GUI 线程接管渲染;
- 触发 resize、scroll 事件,建立媒体查询(执行一个任务中如果生成了微任务,则执行完任务该后就会执行所有的微任务,然后再执行下一个任务)。
- 建立 css 动画(执行一个任务中如果生成了微任务,则执行完该任务后就会执行所有的微任务,然后再执行下一个任务)。
- 执行 requestAnimationFrame 回调(执行一个任务中如果生成了微任务,则执行完该任务后就会执行所有的微任务,然后再执行下一个任务)。
- 执行 IntersectionObserver 回调(执行一个任务中如果生成了微任务,则执行完该任务后就会执行所有的微任务,然后再执行下一个任务)。
- 更新渲染屏幕
- 浏览器判断当前帧是否还有空闲时间,如果有空闲时间,从
requestIdleCallback
回调函数队列中取第一个,执行它。执行微任务队列里的所有微任务,直到requestIdleCallback
回调函数队列清空或当前帧没有空闲时间 - 渲染完毕后,JS 线程继续接管,当前微任务队列的所有 Web Worker 任务,则执行
- 开始下一个宏任务。
注意:
- requestAnimationFrame 和 requestIdleCallback 是和宏任务性质一样的任务,只是他们的执行时机不同而已
- 浏览器在每一轮 Event Loop 事件循环中不一定会去重新渲染屏幕,会根据浏览器刷新率以及页面性能或是否后台运行等因素判断的,浏览器的每一帧是比较固定的,会尽量保持 60Hz 的刷新率运行,每一帧中间
可能会进行多轮事件循环
。 - requestAnimationFrame 是与浏览器是否渲染相关联的。它是在浏览器渲染前,在微任务执行后执行。
- requestIdleCallback 是在浏览器渲染后有空闲时间时执行,如果 requestIdleCallback 设置了第二个参数 timeout,则会在超时后的下一帧强制执行
不同宏任务与微任务队列之间的优先级
- 先执行 macrotasks:I/O -> UI 渲染-> requestAnimationFrame
- 再执行 microtasks:process.nextTick -> Promise -> MutationObserver ->Object.observe
- 再把 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 end
和promise2
,按照先进先出的原则进行输出,微任务执行完毕,检查渲染,交给 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)