JS异步进阶
JS异步编程是整个JS知识中的重点之重 所以在此章节中我会重点讲解JS异步在实际应用中使用场景。
主要内容有:
- event-loop
- promise进阶
- async/await
- 宏任务 & 微任务
1. 先看题目
- 描述 event loop 运行机制(可画图)
- Promise 哪几种状态,如何变化?
- 宏任务和微任务的区别
- 场景题:Promise catch 连接 then
- 场景题:Promise 和 setTimeout 顺序
- 场景题:各类异步执行顺序问题
- 手写 Promise
Promise catch 连接 then
// 第一题
Promise.resolve().then(() => {
console.log(1)
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 第二题
Promise.resolve().then(() => {
console.log(1)
throw new Error('erro1')
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 第三题
Promise.resolve().then(() => {
console.log(1)
throw new Error('erro1')
}).catch(() => {
console.log(2)
}).catch(() => { // 注意这里是 catch
console.log(3)
})
async/await 语法问题
async function fn() {
return 100
}
(async function () {
const a = fn()
const b = await fn()
})()
(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 function async1 () {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2 () {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1()
new Promise (function (resolve) {
console.log('promise1')
resolve()
}).then (function () {
console.log('promise2')
})
console.log('script end')
2. event-loop
简单介绍一下 什么是event-loop: 事件循环/事件轮询
众所周知 JS 是门非阻塞单线程语言,因为在最初 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程的语言话,我们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另一个线程中删除节点),
核心: JS是单线程语言
但是在我们写代码时又会出现很多异步代码 如: setTimeout
DOM事件
ajax请求
核心: 异步要基于回调来实现
那么event-loop本质就是异步回调的实现原理
JS如何执行?
- 从前到后,一行一行执行
- 如果某一行执行报错,则停止下面代码的执行
- 先把同步代码执行完,再执行异步
示例
我们来查看下面这段代码来看看是如何运行的以及结果是怎样的
console.log('hi1')
setTimeout(() => {
console.log('hi2')
}, 1000);
console.log('hi3')
通过画图讲解代码执行过程
执行过程
过程1
- 同步代码,一行一行 放入
call stack
执行 - 遇到异步,会先
记录
等待时机执行(定时器、网络请求) - 时机一到,自动将异步移动至
callback Queue
过程2
- 如
call stack
为空(即同步代码执行完成) Event Loop机制开始工作 - 轮询查询
callback Queue
, 如有则移动至call stack
执行 - 如何进行轮询查找 (永动机一样)
DOM事件和Event Loop
<button id="btn1">提交</button>
<script>
console.log('Hi')
$('#btn1').click(function (e) {
console.log('button clicked')
})
console.log('Bye')
</script>
查看这段代码 DOM事件代码是如何执行的? 也是被event loop调用的 本质还是回调
3. Promise
重点讲解内容:
- 三种状态
- 状态和 then catch
- 常用 API
先回顾一下 Promise 的基本使用
// 加载图片
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
}
)
return p
}
const url = 'https://zeng.pub/cat.svg'
loadImg(url).then(img => {
console.log(img.width)
return img
}).then(img => {
console.log(img.height)
}).catch(ex => console.error(ex))
Promise解决了什么问题? 解决了回调嵌套问题 - 回调地狱
三种状态
三种状态 pending
resolved
rejected
(画图表示转换关系,以及转换不可逆)
// 刚定义时,状态默认为 pending
const p1 = new Promise((resolve, reject) => {
})
// 执行 resolve() 后,状态变成 resolved
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
})
})
// 执行 reject() 后,状态变成 rejected
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject()
})
})
// 直接返回一个 resolved 状态
Promise.resolve(100)
// 直接返回一个 rejected 状态
Promise.reject('some error')
状态和 then catch
状态变化会触发 then catch —— 这些比较好理解,就不再代码演示了
- pending 不会触发任何 then catch 回调
- 状态变为 resolved 会触发后续的 then 回调
- 状态变为 rejected 会触发后续的 catch 回调
then catch 会继续返回 Promise ,此时可能会发生状态变化!!!
// then() 一般正常返回 resolved 状态的 promise
Promise.resolve().then(() => {
return 100
})
// then() 里抛出错误,会返回 rejected 状态的 promise
Promise.resolve().then(() => {
throw new Error('err')
})
// catch() 不抛出错误,会返回 resolved 状态的 promise
Promise.reject().catch(() => {
console.error('catch some error')
})
// catch() 抛出错误,会返回 rejected 状态的 promise
Promise.reject().catch(() => {
console.error('catch some error')
throw new Error('err')
})
看一个综合的例子,即那几个面试题 试试看
// 第一题
Promise.resolve().then(() => {
console.log(1)
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 第二题
Promise.resolve().then(() => {
console.log(1)
throw new Error('erro1')
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 第三题
Promise.resolve().then(() => {
console.log(1)
throw new Error('erro1')
}).catch(() => {
console.log(2)
}).catch(() => {
console.log(3)
})
常用 API
Promise.all 执行所有Promise函数当所有状态不为pending
时返回结果then
Promise.race 执行所有Promise函数当任意一个状态不为pending
是返回结果then
4. async/await
异步代码处理史:
- 异步回调 callback hell
- Promise then catch 链式调用,但也是基于回调函数
- async/await是同步语法, 彻底消灭回调函数
语法介绍
用同步的方式,编写异步。
function loadImg(src) {
const promise = new Promise((resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
reject(new Error(`图片加载失败 ${src}`))
}
img.src = src
})
return promise
}
async function loadImg1() {
const src1 = 'https://zeng.pub/cat.svg'
const img1 = await loadImg(src1)
return img1
}
async function loadImg2() {
const src2 = 'https://avatars3.githubusercontent.com/u/9583120'
const img2 = await loadImg(src2)
return img2
}
(async function () {
// 注意:await 必须放在 async 函数中,否则会报错
try {
// 加载第一张图片
const img1 = await loadImg1()
console.log(img1)
// 加载第二张图片
const img2 = await loadImg2()
console.log(img2)
} catch (ex) {
console.error(ex)
}
})()
和 Promise 的关系
- async 函数返回结果都是 Promise 对象(如果函数内没返回 Promise ,则自动封装一下)
async function fn2() {
return new Promise(() => {})
}
console.log( fn2() )
async function fn1() {
return 100
}
console.log( fn1() ) // 相当于 Promise.resolve(100)
- await 后面跟 Promise 对象:会阻断后续代码,等待状态变为 resolved ,才获取结果并继续执行
- await 后续跟非 Promise 对象:会直接返回
(async function () {
const p1 = new Promise(() => {})
await p1
console.log('p1') // 不会执行
})()
(async function () {
const p2 = Promise.resolve(100)
const res = await p2
console.log(res) // 100
})()
(async function () {
const res = await 100
console.log(res) // 100
})()
(async function () {
const p3 = Promise.reject('some err')
const res = await p3
console.log(res) // 不会执行
})()
- try...catch 捕获 rejected 状态
(async function () {
const p4 = Promise.reject('some err')
try {
const res = await p4
console.log(res)
} catch (ex) {
console.error(ex)
}
})()
总结来看:
- async 封装 Promise
- await 处理 Promise 成功
- try...catch 处理 Promise 失败
异步本质
await 是同步写法,但本质还是异步调用。
核心: 只是语法糖 不改代码运行本质
async function async1 () {
console.log('async1 start')
await async2()
console.log('async1 end') // 关键在这一步,它相当于放在 callback 中,最后执行
}
async function async2 () {
console.log('async2')
}
console.log('script start')
async1()
console.log('script end')
即,只要遇到了 await
,后面的代码都相当于放在 callback 里。 最终还是需要 EventLoop
5. 宏任务与微任务
宏任务: macroTask 微任务: microTask
什么是宏任务? 什么是微任务?
宏任务:setTimeout setInterval DOM 事件
微任务:Promise(对于前端来说)
微任务比宏任务执行的更早 先记住~~~
下面查看一段代码:
console.log(100)
setTimeout(() => {
console.log(200)
})
Promise.resolve().then(() => {
console.log(300)
})
console.log(400)
event loop 和 DOM 渲染
再次回顾 event loop 的过程
- JS是单线程的, 而且和DOM渲染公用一个线程
- JS执行时,得留一些时机供DOM渲染
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
.append($p1)
.append($p2)
.append($p3)
console.log('length', $('#container').children().length )
代码有执行 并且也有结果 但是DOM是没有渲染的
那么DOM是什么时候渲染的?
- 每一次 call stack 结束,都会触发 DOM 渲染(不一定非得渲染,就是给一次 DOM 渲染的机会!!!)
- 然后再进行 event loop
以下列代码为例:
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
.append($p1)
.append($p2)
.append($p3)
console.log('length', $('#container').children().length )
// (alert 会阻断 js 执行,也会阻断 DOM 渲染,便于查看效果)
alert('本次 call stack 结束,DOM 结构已更新,但尚未触发渲染')
// 到此,即本次 call stack 结束后(同步任务都执行完了),浏览器会自动触发渲染,不用代码干预
// 另外,按照 event loop 触发 DOM 渲染时机,setTimeout 时 alert ,就能看到 DOM 渲染后的结果了
setTimeout(function () {
alert('setTimeout 是在下一次 Call Stack ,就能看到 DOM 渲染出来的结果了')
})
宏任务和微任务的区别
- 宏任务:DOM 渲染后再触发
- 微任务:DOM 渲染前会触发
以下列代码为例:
// 修改 DOM
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
.append($p1)
.append($p2)
.append($p3)
// // 微任务:渲染之前执行(DOM 结构已更新)
// Promise.resolve().then(() => {
// const length = $('#container').children().length
// alert(`micro task ${length}`)
// })
// 宏任务:渲染之后执行(DOM 结构已更新)
setTimeout(() => {
const length = $('#container').children().length
alert(`macro task ${length}`)
})
为何微任务执行更早
微任务:ES 语法标准之内,JS 引擎来统一处理。即,不用浏览器有任何关于,即可一次性处理完,更快更及时。
宏任务:ES 语法没有,JS 引擎不处理,浏览器(或 nodejs)干预处理。
简单总结:微任务是ES标准会被 eventloop 优先执行
6. 问题解答
简答题
描述 event loop 运行机制(可画图)
自行回顾event loop的过程 以及与DOM渲染关系 宏任务和微任务eventloop不同的处理
宏任务和微任务的区别
宏任务: setTimeout, setInterval, Ajax, DOM事件
微任务: Promise, async/await
微任务执行时机比宏任务早
Promise的三种状态,如何变化
- pending resolved rejected
- pending -> resolved或rejected -> resolved
- 变化不可逆
场景题
- Promise catch 连接 then
- async/await 语法问题
- Promise 和 setTimeout 顺序
- 执行顺序问题
具体代码演示
手写 Promise 【可选】
考察点: 对promise的理解、编程能力
实现: 1. 初始化&异步调用 2. then catch链式调用 3. API: .resolve .reject .all .race
具体代码演示效果
7. 总结
本章节主要介绍目前热门JS异步面试题:
eventloop promise进阶 async/await 微任务/宏任务
- eventloop: 执行过程、与DOM事件的关系、与DOM渲染的时机
- Promise进阶: 三种状态、then与catch调用时机
- async/await: 解决了什么问题、与Promise的关系
- 微任务/宏任务: 微宏任务有哪些、为什么微任务触发时机更早,与DOM渲染的关系