相关阅读:
一、生成器
1. 概念
(1) 作用
生成器可以暂停、恢复程序执行,搭配 Promise
可以用代码逻辑更紧凑、更易读的方式解决回调地狱。
(2) 关键字 yield
需要写在生成器函数内,当执行时遇见 yield
关键字时,先执行完 yield
声明的代码,然后暂停函数的执行
(3) 定义生成器
在关键字 function
和函数名中间加上符号 *
, 即可定义一个生成器,如: function * generator(){}
(4) 生成器函数的调用
调用生成器函数时,并不会真正的执行生成器函数,而是会返回生成器的迭代器对象( Iterator 接口
)
(5) 迭代器的 next 方法
调用 next
方法,可以执行或恢复被 yield
暂停的生成器函数,next
方法最终会返回一个迭代器结果对
象( IteratorResult 接口
),其属性值取决于以下两种情况:
执行中碰见 yield | value 属性值 | done 属性值 |
---|---|---|
是 | yield 声明的内容 | false,代表生成器函数被暂停,可以再次调用 next 方法 |
否 | 函数返回值 | true,代表生成器函数执行完成,不需再调用 next 方法 |
2. 基本使用
(1) 使用生成器可以分为三步:
① 定义生成器
② 调用生成器,得到迭代器对象
③ 调用迭代器的 next
方法,得到 IteratorResult 对象
,来决定后续操作
(2) 基础语法演示
<script>
// 第一步:定义生成器
function * generator(){
console.log('1')
console.log('2')
console.log('3')
yield '这句话会被返回'
console.log('4')
}
// 第二步:调用生成器,得到迭代器对象
let genIterator = generator()
// 第三步:调用迭代器的 next 方法,得到 IteratorResult 对象
const first = genIterator.next()
// 打印 IteratorResult 对象
console.log("调用 next 后,返回的 IteratorResult 对象为: ")
console.log("value: " + first.value)
console.log("done: " + first.done)
// 继续调用 next ,恢复被 yield 暂停的函数
if(!first.done){
genIterator.next()
}
// 输出:
// 1
// 2
// 3
// 调用 next 后,返回的 IteratorResult 对象为:
// value: 这句话会被返回
// done: false
// 4
</script>
(3) yield 修饰的内容,暂停恢复后会丢失
来看一段代码,预想输出结果是 ares5k,但实际却是 undefined:
<script>
// 定义函数
function getName(){
return 'ares5k'
}
// 定义生成器
function * generator(){
let name = yield getName()
console.log(name)
}
let genIterator = generator() // 取得迭代器
genIterator.next() // 执行到第一个 yield 后暂停
genIterator.next() // 恢复执行
// 输出:undefined
</script>
通过输出结果可以分析大概流程:
(1) 第一次调用 next
方法时,会在 let name = yield getName()
这一行发生暂停,赋值操作是从右到左,此时右侧执行
完就已经暂停了,左侧赋值操作还未进行
(2) 第二次调用 next
方法,会恢复函数执行,继续执行 let name = yield getName()
的左侧赋值部分,而此时赋的并
不是预期的 getName()
的结果, 而是一个 undefined
,因此可以得出结论,yield
修饰的内容,暂停恢复后会丢失
解决 yield 修饰的内容丢失问题:
既然是在恢复时丢失了值,那么只要在恢复执行的那个 next
中,传入 yield
修饰的值即可,而 yield
修饰的内容,
会在前一个 next
暂停时以迭代器结果对象返回( IteratorResult 接口
),具体实现如下:
<script>
// 定义函数
function getName(){
return 'ares5k'
}
// 定义生成器
function * generator(){
let name = yield getName()
console.log(name)
}
let genIterator = generator() // 取得迭代器
const genIteratorResult = genIterator.next() // 取得迭代器结果对象
genIterator.next(genIteratorResult.value) // 恢复执行,并传入 yield 修饰的内容
// 输出:ares5k
</script>
3. 解决回调地狱
用生成器 + Promise 来实现之前文章里每隔 500 毫秒分别输出 a b c d e 的例子
<script>
// 定义函数
let fn = word => {
return new Promise(resolve => {
setTimeout(function () {
console.log(word)
resolve()
}, 500)
})
}
// 业务的逻辑更紧凑,更易读,就像写同步代码一样
function * generator() {
yield fn('a')
yield fn('b')
yield fn('c')
yield fn('d')
yield fn('e')
}
// 按步骤执行各个任务,
const genIterator = generator()
genIterator.next().value.then(()=>{
genIterator.next().value.then(()=>{
genIterator.next().value.then(()=>{
genIterator.next().value.then(()=>{
genIterator.next()
})
})
})
})
</script>
可以看出,生成器 + Promise 实现的代码,业务部分的代码,逻辑更紧凑,更易读,就像写同步代码一样,但
是在生成器的调用阶段,就很麻烦,甚至又出现了回调地狱,而且调用多少次 next
都是预先定义好的,不灵活
实现生成器的自动执行
分析上面例子中的回调地狱,我们可以发现它与平时的回调地狱不同,它的回调内部没有很多的业务逻辑代码,仅
仅是调用 next
恢复函数执行而已,了解了这个点,我们就可以对其进行改造:
<script>
// 定义函数
let fn = word => {
return new Promise(resolve => {
setTimeout(function () {
console.log(word)
resolve()
}, 500)
})
}
// 业务的逻辑更紧凑,更易读,就像写同步代码一样
function * generator() {
yield fn('a')
yield fn('b')
yield fn('c')
yield fn('d')
yield fn('e')
}
// 生成器的自动执行器
function auto(generator){
function next(data){
const result = genIterator.next(data)
if (result.done) return result.value
result.value.then(function(data){
next(data)
})
}
const genIterator = generator();
next();
}
// 调用生成器的自动执行器
auto(generator);
</script>
改造后的代码清爽,易读
理解了上面自动执行器的原理,就可以在实际开发中根据需求对其做出调整,来适应自己的场景
二、async / await
1. 作用
async
+ await
是比使用生成器或 Promise 更简洁的回调地狱处理方案,也有人把其看作生成器 + Promise + 自执行的语法糖。
2. 关键字 async
(1) 定义 async 函数
在关键字 function
或 箭头函数的参数列表
前写 async
,如:async function fn(){}
或 const fn = async () => {}
(2) async 函数的返回值
不管代码中怎么写,async 函数都一定会返回一个 Promise 对象,具体规则如下:
async 函数中定义的返回值 | async 函数的实际返回值 | Promise 对象内部状态 |
---|---|---|
Promise 对象 | 直接返回该 Promise 对象 | 状态由返回的 Promise 对象决定 |
Promise 以外的值 | 返回 Promise,原返回值会当作参数传入处理程序 | 对象状态为 fulfilled |
未显式返回内容 | 返回 Promise,不会对处理程序传参 | 对象状态为 fulfilled |
函数发生异常 | 返回 Promise 对象,异常原因会当作参数传入处理程序 | 对象状态为 rejected |
(3) async 函数返回场景列举
<script>
// 场景一:函数执行异常
const withError = async function () {
throw Error
return '异常后的内容不会被执行'
}
withError().catch(data => {
console.log('场景一:函数执行异常时,会返回一个状态为 rejected 的 Promise 对象,异常原因:' + data)
})
// 场景二:未显式返回内容
const nothing = async function () {
}
nothing().then(() => {
console.log('场景二:未显式返回内容时,会返回一个状态为 fulfilled 的 Promise 对象,且处理程序无参')
})
// 场景三:显式返回 Promise 以外的值
const notPromise = async function () {
return '这是原返回值,现被当作参数传入处理程序'
}
notPromise().then(data => {
console.log('场景三:显式返回 Promise 以外的值时,会返回一个状态为 fulfilled 的 Promise 对象,处理程序参数:' + data)
})
// 场景四:显式返回 状态为 fulfilled 的 Promise 对象
const fulfilledPromise = async function () {
return new Promise(resolve => {
resolve('成功')
})
}
fulfilledPromise().then(data => {
console.log('场景四:显式返回 状态为 fulfilled 的 Promise 对象,直接返回该对象')
})
// 场景五:显式返回 状态为 rejected 的 Promise 对象
const rejectedPromise = async function () {
return new Promise((resolve, reject) => {
reject('失败')
})
}
rejectedPromise().catch(data => {
console.log('场景五:显式返回 状态为 rejected 的 Promise 对象')
})
</script>
3. 关键字 await
(1) 概念
必须写在 async
函数内,当执行到 await
语句时,await
修饰的语句会正常执行,这个语句执行完成后,该 async
函数会将其余代码加入到 <微队列> 中,在主线程任务空闲时,再恢复 async
函数剩余部分的执行。
await
修饰的内容:
① await
修饰 Promise 对象时:
Ⅰ. Promise 对象必须调用 resolve
或 reject
, 否则该 await
之后的代码不会执行
Ⅱ. await
修饰 Promise 语句之后的代码会一直等待,直到 Promise 调用完 resolve
或 reject
后在执行
② await
修饰非 Promise 对象时:
Ⅰ. 不管 await
修饰的代码是否执行完成,之后的代码都会同步执行,不会等待
(2) await 场景列举,进一步熟悉 await 特点
① 场景一:await
标准执行顺序
await
修饰的内容执行 -> 主线程逻辑执行 -> async
函数剩余代码执行 -> setTimeout
执行
<script>
setTimeout(function () {
console.log(1)
})
const noResolveOrReject = async function () {
await console.log(2)
console.log(3)
console.log(4)
}
noResolveOrReject()
console.log(5)
// 输出:2,5,3,4,1
</script>
② 场景二:await
修饰 Promise 对象,后面的代码会一直等待,直到 Promise 调用完 resolve
或 reject
后在执行
如果 await
前是赋值操作,那么 resolve
或 reject
的参数,会赋值给该变量
await
修饰的内容执行 -> 主线程逻辑执行 -> 回到 async
函数 -> 函数内剩余代码等待调用 resolve
或 reject
后执行
<script>
const noResolveOrReject = async function () {
const value = await new Promise(resolve => {
console.log(1)
setTimeout(() => resolve('会返回给 await 的数据'), 3000)
})
console.log(value)
console.log(2)
console.log(3)
}
noResolveOrReject()
console.log(4)
// 输出:1,4,会返回给 await 的数据,2,3
</script>
③ 场景三:await
修饰的 Promise 未调用 resolve
或 reject
语句,async
函数的剩余代码不会执行
await
修饰的内容执行 -> 主线程逻辑执行 -> 回到 async
函数 -> 函数内剩余代码不会执行
<script>
setTimeout(function () {
console.log(1)
})
const noResolveOrReject = async function () {
await new Promise(() => console.log(2))
console.log(3)
console.log(4)
}
noResolveOrReject()
console.log(5)
// 输出:2,5,1
</script>
④ 场景四:await
修饰的是非 Promise 对象,不管其是否执行完,后面的代码都会同步执行
await
修饰的内容执行 -> 主线程逻辑执行 -> 回到 async
函数 -> 剩余代码不会等待 setTimeout
直接执行
<script>
const noResolveOrReject = async function () {
await setTimeout(() => console.log(1), 3000)
console.log(2)
console.log(3)
}
noResolveOrReject()
console.log(4)
// 输出:4,2,3,1
</script>
4. 常见案例
按顺序执行异步任务
<script>
function task1() {
return new Promise(resolve => {
console.log('异步任务1')
resolve('第一个任务结果')
})
}
function task2(param) {
console.log('异步任务2:接收到第一个任务结果:' + param)
}
async function logic() {
const value = await task1()
task2(value)
}
logic()
// 输出:
// 异步任务1
// 异步任务2:接收到第一个任务结果:第一个任务结果
</script>