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 任务,则执行
- 开始下一个宏任务。 注:
- 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)