JS 异步进阶
event loop(事件循环/事件轮询)
首先需要知道的是
1. js是单线程运行的
2. 异步要基于回调来实现
3. event loop就是异步回调的实现原理
js 是如何执行的
1. 从前到后,一行一行执行
2. 如果某一行执行报错,则停止下面代码的执行
3. 先把同步代码执行完,再执行异步
Event Loop 的定义
event loop是js处理异步任务的核心机制,它决定了代码的执行顺序,确保单线程js也能高效处理异步任务,保证不会因为某个任务阻塞了整个页面
Event Loop 执行过程
1. 同步代码,一行一行放在call stack 执行
2. 遇到异步,会先“记录”下,等待时机(定时、网络请求)
3. 时机到了,就移动到callback queue中
4. 如果call stack为空(即同步代码执行完毕),Event Loop开始工作
5. 轮询查询callback queue, 如有则移动到call stack执行
6. 然后继续轮询查找(永动机一样)
每次时间循环(一次event loop)的执行顺序
- 执行同步代码
- 执行所有微任务
- dom渲染
- 执行一个宏任务
- 回到步骤2(执行所有微任务)
- 执行下一个宏任务
- 重复循环
DOM 事件和 event loop
1. js是单线程的
2. 异步(setTimeout, ajax, promise)使用回调,基于event loop
3. dom事件(click, keydown等)也使用回调,基于event loop
宏任务 macroTask 和微任务 microTask
宏任务
宏任务是主流程的任务单元,每次时间循环都会从任务队列取出一个宏任务执行,然后执行所有微任务
包含
| API | 环境 | 说明 |
|---|---|---|
| setTimeout | 浏览器&Node.js | 设定定时器,到时间后执行 |
| setInterval | 浏览器&Node.js | 每隔一定时间执行一次 |
| setImmediate | Node.js | 当前时间循环结束后执行(类似于 setTimeout(0)),但更快 |
| requestAnimationFrame | 浏览器 | 下一帧前执行,大约 16ms 一次 |
| requestIdleCallback | 浏览器 | 浏览器由空闲时间时执行 |
| DOM事件 | 浏览器 | click、keydown等 |
| I/0 任务 | Node.js | 文件读取,网络请求等 |
| UI 渲染任务 | 浏览器 | 由浏览器决定合适执行页面重绘 |
微任务
微任务是更小粒度的任务单元,优先级更高,会在当前宏任务执行完后,紧接着执行,微任务优先级要高于宏任务
包含
| API | 环境 | 说明 |
|---|---|---|
| Promise.then()/Promise.catch()/Promise.finally() | 浏览器&Node.js | Promise 解析后执行 |
| async/await | 浏览器&Node.js | Promise 解析后执行 |
| MutationObserver | 浏览器 | 监听 DOM 变动,触发回调 |
| queueMicrotask | 浏览器&Node.js | 手动创建微任务 |
| process.nextTick() | Node.js | Node.js 特有,优先级比普通微任务更高,在当前阶段执行 |
浏览器的渲染时机
浏览器一般在每一帧进行一次页面渲染,默认目标是60fs,即16.67ms进行一次渲染,但event loop的任务调度可能会影响渲染时机
一次完整的浏览器帧
- 执行js(同步任务+微任务+谁宏任务)
- 计算样式
- 布局
- 绘制
- 合成
- 浏览器空闲时间,可能触发requestIdleCallback
- 下一帧的开始
总结:
- UI渲染通常发生在所有同步任务和微任务执行完毕后,而宏任务则在UI渲染发生之后才会触发
- requestAnimationFrame会在下一帧渲染前执行
- 微任务执行过多会可能会阻塞UI渲染(微任务无限递归)
- 过场的同步任务会阻塞渲染,导致页面卡顿
- 同步代码 -> 微任务代码 -> 页面渲染 -> 宏任务代码 -> 微任务代码(下一event loop) -> 页面渲染 -> ....
- 微任务是ES6语法规定的 ,宏任务是浏览器规定的
requestIdleCallback
执行时机:
- 浏览器一帧的主要任务完成后(同步代码、微任务、渲染等),如果还有时间,就执行
- 如果没有足够的空闲时间,可能会被延迟到下一帧或更晚执行
- 如果设置了超过时间,超过改时间后无论浏览器是否空闲,都会执行
Promise
三种状态:
pending、fulfilled、rejected
pending ---> fulfilled 或者 pending ----> rejected
变化不可逆
状态表现:
1. pending状态, 不会出发then和catch
2. fulfilled状态,会触发后续的then回调函数
3. rejected状态,会触发后续的catch回调函数
then 和 catch 改变状态
1. then正常返回fulfilled,里面有报错则返回rejected
2. catch正常返回fulfilled,里面有报错则返回rejected
async/await
async/await 是 js 处理异步操作的现代方法,基于 Promise,但比then/catch更简洁、直观,让代码像 同步代码 一样可读
try..catch 可捕获异常,待体力 Promise 的catch
async
-
async用于声明一个函数,使其总是返回一个Promise; - 这个Promise的状态由
return的值决定:
1. 如果return一个值,会封装成Promise.resolve(value)
2. 如果throw一个错误,会封装成Promise.reject(error)
await
-
await只能在async函数内使用,相当于Promise.then -
await让js 等待Promise解析(fulfilled)后,才继续执行 -
await暂停当前函数的执行(包含同层级后面的代码,是指await下面那行的代码),不会阻塞整个线程