事件循环机制EventLoop
Event Loop 即事件循环,是浏览器或Node解决单线程运行不阻塞的一种机制
大纲链接 §
[toc]
进程和线程
- 浏览器打开一个页面就相当于开一个进程
- 在进程中可以同时做很多事情,每一个事情都有一个“线程”去处理
- 所以一个进程中可包含多个线程
浏览器中的多线程
浏览器中一般包含以下线程:
- GUI 渲染线程:渲染页面 & 绘制图形
- 绘制页面,解析HTML、CSS,构建DOM树等
- 页面的重绘和重排
- 与JS引擎互斥(JS引擎阻塞页面刷新)
- JS引擎线程:渲染和解析JS代码
- js脚本代码执行
- 负责执行准备好的事件(任务队列中),例如定时器计时结束或异步请求成功且正确返回的回调
- 与GUI渲染线程互斥
- 事件触发线程:监听事件触发
- 当对应的事件满足触发条件,将事件添加到js的任务队列末尾
- 多个事件加入任务队列需要排队等待
- 定时触发器线程:给定时器计时
- 负责执行异步的定时器类事件:setTimeout、setInterval等
- 浏览器定时计时由该线程完成,计时完毕后将事件添加至任务队列队尾,等待主线程执行
- 异步HTTP请求线程:基于HTTP网络从服务器端获取资源和信息
- 负责异步请求
- 当监听到异步请求状态变更时,如果存在回调函数,该线程会将回调函数加入到任务队列队尾
- WebWorker等
JS事件循环机制
浏览器是多线程的,异步任务借助浏览器的线程和JavaScript的执行机制实现
- JS是单线程语言,浏览器只分配一个线程“JS引擎线程”用来解析运行JS代码,同一时间只能做一件事情
- 单线程执行任务队列:如果前一个任务非常耗时,则后续任务必须一致等待,从而导致程序假死
同步任务和异步任务执行顺序
同步与异步
计算机领域中的同步与异步和中文翻译的同步和异步正好相反
计算机中的同步是连续性的动作,上一步未完成前,下一步会发生堵塞,直至上一步完成后,下一步才可以继续执行
- 为防止某个耗时任务导致程序假死,JS将执行的任务分为两类
- 同步任务
synchronous
- 又称为非耗时任务,指的是在主线程上排队执行的任务
- 只有前一个任务执行完毕,才能执行后一个任务,即按代码顺序执行
- 异步任务
asynchronous
- 又称为耗时任务,异步任务由JS委托给 宿主环境(浏览器/Node.js) 进行执行
- 异步任务首先到 Event Table 进行回调函数注册
- 当异步任务的触发条件满足,将回调函数从Event Table 压入 Event Queue 中
同步任务和异步任务的执行过程
- 同步任务由JS主线程依次执行
- 异步任务委托给宿主环境执行
- 已完成的异步任务对应的回调函数,会被加入到任务队列中等待执行
- JS主线程的执行栈清空后(当前的同步任务执行完成),会依次读取任务队列中的回调函数,放到执行栈中执行(即通知JS主线程执行 Event Queue 中回调函数)
- 只要主线程空了,就会去 Event Queue 读取回调函数
- JS主线程不断重复以上步骤,这个过程被称为 Event Loop
举例
setTimeout(cb, 1000),当1000ms后,就将cb压入 Event Queueajax(请求条件, cb),当http请求发送成功后,cb压入 Event Queue
补充
- 队列:先进先出 (队列弹药夹)
- 栈:后进先出(薯片栈)
JavaScript的异步任务是存在优先级的
宏任务与微任务的概念
除了广义上将任务划分为同步任务和异步任务(耗时任务),异步任务又进一步分为宏任务和微任务:
- 异步宏任务
macrotask- 异步的数据请求:
Ajax/Fetch - 定时器 API
setTimeout/setInterval - 动画回调
requestAnimationFrame“图穷匕见” - 文件操作,即
I/O操作 - 事件绑定/队列
- MessageChannel
- setImmediate[NODE]
- history traversal任务(h5当中的历史操作)
- 其他
- 异步的数据请求:
- 异步微任务
microtaskPromise.then()、.catch()和.finally();Promise.all()、Promise.any()、Promise.allSettled()、Promise.race()等async/awaitqueueMicrotaskMutationObserver(h5新增,用来监听DOM节点变化的)IntersectionObserverrequestAnimationFrameprocess.nextTick(Node.js)- 其他
实例化 new Promise(Cb) 实例中的回调函数Cb为同步任务
总结 Event Loop 执行过程
注意事项
- 每一个宏任务执行完之后,都会检查是否存在待执行的微任务,如果有,则执行完所有微任务之后,再继续执行下一个宏任务
- 宏任务和微任务是交替执行的
- 宏任务和微任务分别有各自的任务队列 Event Queue,即宏任务队列和微任务队列
执行过程
- 代码开始执行,创建一个全局调用栈,script 作为宏任务执行
- 执行过程过同步任务立即执行,异步任务根据异步任务类型分别注册到微任务队列和宏任务队列
- 同步任务执行完毕,查看微任务队列
- 若存在微任务,将微任务队列全部执行(包括执行微任务过程中产生的新微任务)
- 若无微任务,查看宏任务队列,执行第一个宏任务,宏任务执行完毕,查看微任务队列,重复上述操作,直至宏任务队列为空
举例分析宏任务和微任务的执行过程
示例
|
|
基于then返回的 promise 实例的状态和值,主要看 onFulfilled/onrRejected 是否执行
- 函数返回的不是 promise 实例:
- 方法执行不报错,p2 状态是 成功,值即返回值
- 方法执行报错,则 p2 是失败的,值是报错原因
- 函数返回的是 promise 实例:则这个实例的状态和值决定了 p2 的状态和值
示例:
|
|
new Promise(Cb)实参 回调函数:Cb是同步代码,立即执行- 回调函数:Cb的形参
resolvereject- 一旦执行
resolve()或reject()就会将状态变为对应的resolved或rejected,不再改变 - 一般会使用分支语句或者
try..catch..分别调用resolve()、reject()
- 一旦执行
调用
resolve()的时机,示例:
|
|
- 在同步代码中调用
resolve(),之后的 .then 方法都为微任务,按顺序添加到微任务队列中 - 在宏任务中调用
resolve(),之后的 .then 方法都为宏任务中的微任务,需在当前宏任务执行之后,按顺序添加到微任务队列中
示例:
|
|
- 将
setTimeout放到宏任务队列 - 执行同步任务
new Promise(function (resolve) {}) - 将
.then()放到微任务队列 - 执行同步任务
console.log('2') - 执行微任务队列中所有微任务
.then() - 执行下一个宏任务
setTimeout
总结影响异步代码执行顺序的因素
- 同步代码的耗时
- 异步微任务队列
- 异步宏任务队列
- I/O 宏任务之后的微任务 .then()
- setTimeout 的耗时影响回调被添加到任务队列的时机
- 在 setTimeout 回调函数中调用 resolve(),影响依赖此实例的微任务 .then()
加强示例
|
|
- await 需要等待后面的 promise 实例是状态为成功时,才会执行之后的代码
- 首先 当前上下文中,await 之后的代码都是异步微任务 @aw
- 如果已经知道 await 后面的实例状态是成功的话
- 则 @aw 直接放在Event Queue中,等待执行即可
- 如果 await 后面实例状态是失败的话
- 则 @aw 在 Web API 中永远不会进入到 Event Queue中,因为永远不会执行
- 如果暂时还不知道是成功还是失败,则 @aw 先放置在 Web API 中
- 等到知道实例状态是成功后,再挪至到 Event Queue 中等待执行
总结EventLoop的概念及经典面试题
JS主线程中任务队列中读取异步任务的回调函数,放到执行栈中依次执行,这个过程是循环不断的,整个机制又称为 EventLoop 事件循环
结合 EventLoop 分析输出的顺序
|
|
分析
- A D 属于同步任务,根据代码的先后顺序依次被执行
thenFs.readFile异步任务,具体为**I/O任务**,属于宏任务,委托给宿主环境执行setTimeout异步任务,委托给宿主环境执行thenFs.readFile消耗耗一定的时间后将回调函数放入任务队列,setTimeout延迟 0,立即将回调放入任务队列- 读文件的过程不可能是0秒,最快也是几毫秒,这样就慢于
setTimeout执行完毕 I/O任务和setTimeout同样是宏任务,按先来顺序执行,但 B 是在 I/O 宏任务之后的 promise 微任务里面打印的,所以应该先打印C- 任务队列先进先出,首先执行的是
setTimeout的回调函数,后执行的是thenFs.readFile的回调函数
小结
- I/O 任务和 setTimeout 同样是宏任务,按先后顺序执行
- I/O 宏任务里面的 微任务 .then() 需要等待当前宏任务执行完毕,在依次添加到微任务队列中等待执行
- 异步任务操作的耗时影响着其回调函数被加入到任务队列的先后顺序
setTimeout耗时由第二个参数决定thenFs.readFile耗时不定,取决于文件
关于 setTimeout 耗时
|
|
- Web API任务事件监听队列,定时器任务,浏览器开始分配一个定时器监听线程,计时完毕,将回调函数放到宏任务队列中
- 多个定时器,第二个参数决定将回调函数添加到宏任务队列中的先后顺序
- 定时器第二个参数相同时,则按代码顺序添加
- 一般情况,同步代码的耗时不考虑在内;如果定时器第二个参数为毫秒级别,耗时相近的定时器任务不能确定哪一个先添加到红任务队列中
异步经典面试题
第一题:
|
|
分析 执行
await async2()
- 先得到
await右侧表达式的结果- 即执行
async2(),打印同步代码console.log('async2') - 并且
return Promise.resolve(undefined)
- 即执行
await表达式执行后,将后续代码作为微任务推入微任务队列(可以标记为 microtask A)- 再中断
async函数,先执行async1()外的同步代码(包括如果后面有 .then也中断执行)
分析 执行
new Promise()
- Promise 构造函数是直接调用的同步代码,所以
console.log( 'promise1' )
代码运行到
promise.then()
- 是微任务,所以暂时不打印
- 只是推入当前宏任务的微任务队列中
- 微任务会在当前宏任务的同步代码执行完毕,才会依次执行
同步代码执行完毕,开始执行微任务队列
- 回到
async内部,执行await Promise.resolve(undefined) - 如果一个
Promise对象被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果,即从Promise对象中 解包出 返回值 - 本例中就是
Promise.resolve(undefined)正常处理完成,并返回其处理结果undefined
小结1
- 将语句的执行类型分为 同步 和 异步
- 异步又分为 宏任务 和 微任务
async function xxx() {}异步函数中,关键字await第一次出现之前的语句,为同步执行,包括 首个await修饰的语句也是同步执行( await 操作符是从右往左执行)- 首个
await语句的后续代码为异步执行- 可看做是微任务,依次添加到微任务队列
- 如果一个
Promise对象被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果,即从Promise对象中 解包出 返回值 await表达式的后续代码被阻塞后,要执行async之外的代码- await 操作符用于等待一个Promise 对象。只能在异步函数 async function 中使用
[返回值] = await 表达式;- 表达式为一个 Promise 对象或者任何要等待的值
- 返回值为返回 Promise 对象的处理结果
- 如果等待的不是 Promise 对象,则返回该值本身
小结2
new Promise(function (resolve) {})Promise 实例化语句中的回调函数function (resolve) {}是 同步执行new Promise(function (resolve) {})中 如果不调用resolve(),就永不执行 后续.then的回调.then()的回调函数为异步执行,添加到 微任务队列- 使用
new Promise(),调用 resolve 后,需要在 .then 的第一个参数里,才能拿到结果- 即调用
resolve时,会把 .then 的参数推入微任务队列,等主线程空闲时,再执行
- 即调用
思考如果代码变为以下,该如何执行
|
|
解异步执行顺序题的公式,准备以下几个框:
- 主线程执行栈
- 微任务队列
- 宏任务队列
- Web API (定时器等)
- Event Queue
按照代码顺序将相应类型的语句依次添加到 执行栈->微任务队列->宏任务队列
依次执行 执行栈中的语句->微任务队列中的语句->宏任务队列中的语句 问题:
- async 做一件什么事情
- await 在等什么
- await 等到之后,做了一件什么事情
- async/await 比 promise有哪些优势
async 做一件什么事情
带 async 关键字的函数,使得函数的返回值必定是 promise 对象
|
|
- 仅是把return值包装成了promise对象
- 如果async关键字函数返回的不是promise,会自动用
Promise.resolve()包装 - 如果async关键字函数显式地返回promise,那就以返回的promise为准
- 如果async关键字函数返回的不是promise,会自动用
- 在语义上,async表示函数内部有异步操作
- await 关键字要在 async 关键字函数的内部,await 写在外面会报错
await 在等什么
await等的是 右侧「表达式」的结果
- 右侧如果是函数,那么这个函数的return值就是「await表达式的结果」
右侧如果是一个 常量,那await表达式的结果就是 这个常量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19async function async1() { console.log( 'async1 start' ) await async2() console.log( 'async1 end' ) } async function async2() { console.log( 'async2' ) } async1() console.log( 'script start' ) /* async1 start async2 script start async1 end */await表达式 会让出线程,阻塞 await表达式后面 的代码从右向左,先执行async2后,发现有await关键字,于是让出线程,阻塞 await表达式代码后面的代码(如果有)
先打印async2 (同步),后打印的script start(同步)
await 等到之后,做了一件什么事情
等到之后,对于await来说,分2个情况
- 不是promise对象
- 是promise对象
分析
- 如果不是 promise , await会阻塞 await表达式 后面的代码,先执行async 函数外面的同步代码
- 同步代码执行完,再回到async内部,把这个非promise的东西,作为 await表达式的结果 (微任务)
- 如果它等到的是一个 promise 对象,await 也会暂停async 函数后面的代码,先执行async函数外面的同步代码
- 等到 Promise 对象 fulfilled后
- 把 resolve 的参数作为 await 表达式的运算结果 (微任务)
解析宏任务、微任务的执行过程
- 如果将整个script代码块执行的代码看做是一个 宏任务
- 则先执行 这个宏任务中的同步代码
- 如果执行中遇到 setTimeout 之类宏任务,就把这个 setTimeout 内部的函数推入「宏任务的队列」中,下一轮宏任务执行时调用
- 如果执行中遇到 promise.then() 之类的微任务,就推入到「当前宏任务的微任务队列」中,在本轮宏任务的同步代码执行都完成后,依次执行微任务队列中所有的微任务
async/await 比 promise有哪些优势
第二题:
|
|
第三题:
|
|
重写代码顺序,函数声明提前
- 声明 async function foo
- 声明 async function errorFunc
声明 function bar
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41// 重写代码顺序 async function foo() { await bar() console.log('async1 end') } async function errorFunc() { try { await Promise.reject('error!!!') // 将 promise 对象的状态锁定为失败 // await 之后的代码都是异步执行 } catch (e) { console.log(e) // 异步微任务 相当于promise.catch } console.log('async1') return Promise.resolve('async1 success') } function bar() { console.log('async2 end') } console.log('script start') setTimeout(() => { console.log('time1') }, 1 * 2000) Promise.resolve() .then(function () { console.log('promise1') }) .then(function () { console.log('promise2') }) foo() errorFunc() .then(res => console.log(res)) console.log('script end')
将函数声明带入函数执行位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32console.log('script start') setTimeout(() => { console.log('time1') }, 1 * 2000) Promise.resolve() .then(function () { console.log('promise1') }) .then(function () { console.log('promise2') }) (async function foo() { await console.log('async2 end') console.log('async1 end') })() (async function errorFunc() { try { await Promise.reject('error!!!') // 将 promise 对象的状态锁定为失败 // await 之后的代码都是异步执行 } catch (e) { console.log(e) // 异步微任务 相当于promise.catch } console.log('async1') return Promise.resolve('async1 success') })() .then(res => console.log(res)) console.log('script end')
总结解题顺序
- 使执行顺序更符合人类阅读顺序
- 重写代码顺序
- 将函数声明带入函数执行位置
- 标记每一个
setTimeoutA B C...thenA B C... - 标记每一个
setTimeoutA B C...thenA B C... - 标记每一个
setTimeoutA B C...thenA B C... - setTimeout 添加到 Web API 时,注意第二个参数的耗时,影响添加到异步宏任务队列的先后顺序
new Promise(Cb)中的回调 Cb 为同步执行- Cb 参数的
resolve和reject执行时机,以resolve为例 resolve同步执行resolve异步执行resolve在异步微任务resolve在异步宏任务1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82console.log('script start') // setTimeoutA setTimeout(() => { console.log('time1') }, 1 * 2000) Promise.resolve() // 同步 将 promise 对象的状态锁定为成功 // thenA .then(function () { console.log('promise1') }) // 带 thenB 等待promise的状态改变 运行到此时在第二轮微任务队列末尾添加thenB 的微任务 // thenB .then(function () { console.log('promise2') }) (async function foo() { await console.log('async2 end') // 同步 首个await console.log('async1 end') // 添加到异步微任务队列 })() (async function errorFunc() { try { await Promise.reject('error!!!') // 同步 将 promise 对象的状态锁定为失败 // await 之后的代码都是异步执行 } catch (e) { console.log(e) // 捕获错误 console.log(error!!!) 添加到异步微任务队列 相当于 promise.catch } console.log('async1') // 添加到异步微任务队列 return Promise.resolve('async1 success') // 异步 将 promise 对象的状态锁定为成功 值为 'async1 success' })() // 带thenC 运行到此时在第二轮微任务队列末尾添加thenC 的微任务 // thenC .then(res => console.log(res)) console.log('script end') /* - 同步 - console.log('script start'); - console.log('async2 end') - console.log('script end') - Web API - setTimeoutA 2000ms - 异步 - 第一轮微任务队列 - console.log('promise1') // 带 thenB 等待promise的状态改变 运行到此时在第二轮微任务队列末尾添加.then 的微任务 - console.log('async1 end') - console.log('error!!!') - console.log('async1') // 带 thenC 等待promise的状态改变 运行到此时在第二轮微任务队列末尾添加.then 的微任务 - 第二轮微任务队列 - console.log('promise2') // thenB - console.log('async1 success') // thenC - 宏任务队列 - console.log('time1') * */ /* console.log('script start'); console.log('async2 end') console.log('script end') console.log('promise1') console.log('async1 end') console.log('error!!!') console.log('async1') console.log('promise2') console.log('async1 success') console.log('time1') * */ /* script start async2 end script end promise1 async1 end error!!! async1 promise2 async1 success time1 * */
- Cb 参数的
第四题:
|
|
第五题:
|
|
参考文章
相关文章
- 无