JS 异步进阶
什么是 event loop(事件循环/事件轮询)
- JS 是单线程运行的
- 异步要基于回调来实现
- event loop 就是异步回调的实现原理
JS 如何执行?
从前到后,一行一行执行
如果某一行执行报错,则停止下面代码的执行
先把同步代码执行完,再执行异步
event loop 执行过程
event loop 过程 1
同步代码,一行一行放在 Call Stack 执行
遇到异步,会先记录下来,等待时机(定时、网络请求等)
时机到了,就移动到 Callback Queue
event loop 过程 2
- 如果 Call Stack 为空(即同步代码执行完)Event loop 开始工作
- 轮询查找 Callback Queue,如有则移动到 Call Stack 执行
- 然后继续轮询查找(永动机一样)
图解代码执行过程:
console.log('Hi') setTimeout(function cb1() { console.log('cb1') }, 2000) console.log('Bye')
图片出处:https://coding.imooc.com/lesson/400.html#mid=35171
DOM 事件与 event loop
- JS 是单线程
- 异步(setTimeout,ajax等)使用回调,基于 event loop
- DOM 事件也使用回调,基于 event loop
Promise 三种状态
三种状态
- pending resolve rejected
- pending => resolve 或 pending => rejected
- 变化不可逆
状态的表现
- pending 状态,不会触发 then 和 catch
- resolved 状态,会触发后续的 then 回调函数
- rejected 状态,会触发后续的 catch 回调函数
成功的回调:只执行 then,不会执行 catch
// 输出结果:data 100 const p1 = Promise.resolve('100') p1.then(data => { console.log('data', data) }).catch(err => { console.log('err', err) })
失败的回调:只执行 catch,不执行 then
// 输出结果:err2 -100 const p2 = Promise.reject('-100') p2.then(data => { console.log('data2', data) }).catch(err => { console.log('err2', err) })
then 和 catch 改变状态
- then 正常返回 resolved,里面有报错则返回 rejected
- catch 正常返回 resolved,里面有报错则返回 rejected
第一种情况:
const p1 = Promise.resolve().then(() => { return 100 }) console.log('p1', p1); p1.then(() => { console.log('200') }) const p2 = Promise.resolve().then(() => { throw new Error('then error') }) console.log('p2', p2) p2.then(() => { console.log('300') }).catch(err => { console.log('error', err) })
第二种情况:
const p3 = Promise.reject('my error').catch(err => { console.log(err) }) console.log('p3', p3) p3.then(() => { console.log(100) }) const p4 = Promise.reject('my error').catch(err => { throw new Error('catch err') }) console.log('p4', p4) p4.then(() => { console.log(200) }).catch(() => { console.log('some err') })
Promise 关于 then 和 catch 的面试题
示例 1 :resolve 成功输出 1 后又是一个成功的回调(fulfilled),之后执行 then,不会执行 catch
// 输出结果:1 3 Promise.resolve().then(() => { console.log(1) }).catch(() => { console.log(2) }).then(() => { console.log(3) })
示例 2:resolve 成功输出 1 后报错异常(rejected),之后执行 catch 输出 2 后(fulfilled),执行 then
// 输出结果:1 2 3 Promise.resolve().then(() => { console.log(1) throw new Error('error1') }).catch(() => { console.log(2) }).then(() => { console.log(3) })
示例 3:resolve 成功输出 1 后报错异常(rejected),之后执行 catch 输出 2 后(fulfilled),之后就结束了
// 输出:1 2 Promise.resolve().then(() => { console.log(1) throw new Error('error1') }).catch(() => { console.log(2) }).catch(() => { console.log(3) })
async 和 await 基本使用
- 使用 await,必须有 async 包裹
- ! 的作用是防止此前代码结尾不加分号
// 加载图片 function loadImg(src) { const p = new Promise((resolve, reject) => { const img = document.createElement('img') img.onload = () => { resolve(img) } img.onerror = () => { const err = new Error(`图片加载失败${src}`) reject(err) } img.src = src document.body.appendChild(img) }) return p } const src = 'xxx.png' !(async function() { const img = await loadImg(src) console.log(img.width, img.height) // 100 110 })()
async/await 和 Promise 的关系
async/awiat 是消灭异步回调的终极武器
但和 Promise 并不互斥
反而,两者相辅相成
执行 async 函数,返回的是 Promise 对象
await 相当于 Promise 的 then
try…catch 可捕获异常,代替了 Promise 的 catch
示例 1:执行 async 函数,返回的是一个 Promise 对象
async function fn1(){ // return 100 //相当于 return Promise.resolve(100) return Promise.resolve(200) } const res1 = fn1() // 执行 async 函数,返回的是一个 Promise 对象 console.log('res1', res1); res1.then(data => { console.log('data', data) // 200 })
示例 2:
async function fn() { return Promise.resolve(200) } !(async function() { const p1 = Promise.resolve(300) const data = await p1 // await 相当于 Promise 的 then console.log('data', data) })() !(async function() { const data1 = await 400 // await Promise.resolve(400) console.log('data1', data1) })() !(async function() { const data2 = await fn() console.log('data2', data2); })()
示例 3:try…catch 异常捕获
!(async function() { const p4 = Promise.reject('error1') try { const res = await p4 console.log(res) } catch (err) { console.error(err) // try...catch 相当于 promise catch } })()
示例 4:
!(async function() { const p5 = Promise.reject('err') // rejected 状态 const res = await p5 // await -> then console.log('res', res) // 不会执行 })()
异步的本质
- 异步的本质是回调函数
- async/await 是消灭异步回调的终极武器
- JS 是单线程,还得是有异步,还得是基于 event loop
- async/await 是一个很好的语法糖
示例:await 后面的内容是 异步的
async function async1() { console.log('async1 start') // 2 await async2() // undefined // await 的后面,都可以看作是 callback 里的内容,即异步 // 类似,event loop,setTimeout() console.log('async1 end') // 5 } async function async2() { console.log('async2') // 3 } console.log('script start') // 1 async1() console.log('script end') // 4
输出顺序:
示例 2:注——await 后面的都是异步内容,等主线程执行完才会触发 event loop 机制执行
async function async1() { console.log('async1 start') // 2 await async2() // undefined // 下面三行都是异步回调 callback 的内容 console.log('async1 end') // 5 await async3() console.log('async1 end 2') // 7 } async function async2() { console.log('async2') // 3 } async function async3() { console.log('async3') // 6 } console.log('script start') // 1 async1() console.log('script end') // 4 // 同步代码执行完,event loop
宏任务与微任务
- 宏任务:setTimeout、setInterval、Ajax、DOM事件
- 微任务:Promise、async/await
- 微任务执行时机比宏任务要早
Event Loop 和 DOM 渲染
每次 Call Stack 清空(即每次轮询结束)
都是 DOM 重新渲染的机会,DOM 结构如有改变则重新渲染
然后再去触发下一次 Event Loop
图片出处:https://coding.imooc.com/lesson/400.html#mid=35181
宏任务和微任务的区别
- 宏任务:DOM 渲染后触发,如 setTimeout
- 微任务:DOM 渲染前触发,如 Promise
宏任务和微任务的根本区别
- 在 micro task queue 里执行当前的微任务
- 微任务比宏任务的执行时机更早
- 微任务发生在 DOM 渲染之前
- 宏任务发生在 DOM 渲染之后
JS 异步面试题
什么是宏任务和微任务、两者的区别
- 宏任务:setTimeout,setInterval,Ajax,DOM 事件
- 微任务:Promise,async/await
- 微任务执行时机比宏任务要早
场景题 — async、await 语法
- async 修饰的函数返回的是 promise 实例
- a 没有 await 修饰,输出的是 promise 实例
- b 有 await 修饰,相当于 then ,输出的是 100
示例 1:
async function fn() { return 100 } (async function() { const a = fn() const b = await fn() console.log(a) console.log(b) })()
示例 2:失败后面的代码都不会执行
(async function() { console.log('start') const a = await 100 console.log('a', a) const b = await Promise.resolve(200) console.log('b', b) const c = await Promise.reject(300) console.log('c', c) console.log('end') })()
promise 和 setTimeout 的顺序问题
- 先执行主线程的任务
- 再执行微任务
- 再执行宏任务
示例:
console.log(100) setTimeout(() => { console.log(200) }) Promise.resolve().then(() => { console.log(300) }) console.log(400)
async/await 的顺序问题
- 主线程 => 微任务 => 宏任务
- 初始化 promise 时,传入的函数会立刻被执行(主线程任务)
async function async1 () { console.log('async1 start') // 2 await async2() // await 后面的都作为回调内容 —— 微任务 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() // 初始化 promise 时,传入的函数会立刻被执行 new Promise (function (resolve) { console.log('promise1') // 4 resolve() }).then(function () { // 微任务 console.log('promise2') // 7 }) console.log('script end') // 5 // 同步代码执行完毕(event loop —— call stack 被清空) // 执行微任务 // 尝试触发 DOM 渲染 // 触发 Event Loop,执行宏任务
手写 Promise 源码
- 有难度有深度
/** * @description MyPromise * @author 前端杂货铺 */ class MyPromise { state = 'pending' // 状态,'pending' 'fulfilled' 'rejected' value = undefined // 成功后的值 reason = undefined // 失败后的原因 resolveCallbacks = [] // pending 状态下,存储成功的回调 rejectCallbacks = [] // pending 状态下,存储失败的回调 constructor(fn) { const resolveHandler = (value) => { if (this.state === 'pending') { this.state = 'fulfilled' this.value = value this.resolveCallbacks.forEach(fn => fn(this.value)) } } const rejectHandler = (reason) => { if (this.state === 'pending') { this.state = 'rejected' this.reason = reason this.rejectCallbacks.forEach(fn => fn(this.reason)) } } try { fn(resolveHandler, rejectHandler) } catch (err) { rejectHandler(err) } } then(fn1, fn2) { fn1 = typeof fn1 === 'function' ? fn1 : (v) => v fn2 = typeof fn2 === 'function' ? fn2 : (e) => e if (this.state === 'pending') { const p1 = new MyPromise((resolve, reject) => { this.resolveCallbacks.push(() => { try { const newValue = fn1(this.value) resolve(newValue) } catch (err) { reject(err) } }) this.rejectCallbacks.push(() => { try { const newReason = fn2(this.reason) reject(newReason) } catch (err) { reject(err) } }) }) return p1 } if (this.state === 'fulfilled') { const p1 = new MyPromise((resolve, reject) => { try { const newValue = fn1(this.value) resolve(newValue) } catch (err) { reject(err) } }) return p1 } if (this.state === 'rejected') { const p1 = new MyPromise((resolve, reject) => { try { const newReason = fn2(this.reason) reject(newReason) } catch (err) { reject(err) } }) return p1 } } // 就是 then 的一个语法糖,简单模式 catch (fn) { return this.then(null, fn) } } MyPromise.resolve = function (value) { return new MyPromise((resolve, reject) => resolve(value)) } MyPromise.reject = function (reason) { return new MyPromise((resolve, reject) => reject(reason)) } MyPromise.all = function (promiseList = []) { const p1 = new MyPromise((resolve, reject) => { const result = [] // 存储 promiseList 所有的结果 const length = promiseList.length let resolvedCount = 0 promiseList.forEach(p => { p.then(data => { result.push(data) // resolvedCount 必须在 then 里面做 ++ // 不能用 index resolvedCount++ if (resolvedCount === length) { // 已经遍历到了最后一个 promise resolve(result) } }).catch(err => { reject(err) }) }) }) return p1 } MyPromise.race = function (promiseList = []) { let resolved = false // 标记 const p1 = new Promise((resolve, reject) => { promiseList.forEach(p => { p.then(data => { if (!resolved) { resolve(data) resolved = true } }).catch((err) => { reject(err) }) }) }) return p1 }
不积跬步无以至千里 不积小流无以成江海