Promise 在解决异步编程回调地狱的问题时,采用了链式的调用方式,但在代码比较复杂的业务逻辑中还是有可能出现嵌套问题的 → 如 ↓
生成器(Generator)是一种特殊的函数,它可以让我们更加灵活的控制函数再什么时候继续执行、暂停执行等
基本使用
1. 创建生成器函数: 生成器函数通过 "function*" 进行创建 → 在声明函数时,添加一个星号 * 就代表该函数为一个生成器函数
2. yield: 生成器函数通过 yield 关键字来控制函数的执行流程(函数在执行时碰到 yeild 关键字就会停止执行,如果像继续向后执行,需要通过对应生成器函数返回的对象中的 next 方法来继续执行后续的代码)
3. 返回值: 生成器函数返回一个 Generator 对象(生成器事实上是一种特殊的迭代器)
function* gen() { // -- 1. 创建生成器函数
console.log("generator function executed")
yield "kong" // -- 2. 每一个 yield 关键字都会中断函数的执行
console.log("kong ~")
yield "deng"
console.log("deng ~")
yield "wang"
console.log("wang ~")
return "END" // -- 返回值处也会进行中断(即可以理解为也是一个特殊 yield 中断语句)
}
const generator = gen() // -- 3. 调用生成器函数 → 会根据 yeild 关键字,返回一个对应的 generator 对象(特殊的迭代器对象) → log: generator function executed
// -- 4. 通过 next 方法来继续执行后续的代码
console.log(generator.next()) // log: { value: 'kong', done: false } → kong ~
console.log(generator.next()) // log: { value: 'deng', done: false } → deng ~
console.log(generator.next()) // log: { value: 'wang', done: false } → wang ~
console.log(generator.next()) // log: { value: 'END', done: true }
console.log(generator.next()) // log: { value: 'undefined', done: true }
next 函数传参
我们可以在调用 next 函数时,给其传递参数,该参数会作为上一个 yield 语句的返回值
即: 我们可以通过该方式来使其异步请求,当请求完成时,我们在执行后续的代码,并将对应异步的结果返回到对应的位置上(生成器函数异步转同步)
如下示例
function* gen(prefix) {
const value1 = yield prefix + "_aaa"
const value2 = yield value1 + "_bbb"
const vlaue3 = yield value2 + "_ccc"
return vlaue3
}
const generator = gen("kong")
const res1 = generator.next()
const res2 = generator.next(res1.value)
const res3 = generator.next(res2.value)
const result = generator.next(res3.value)
console.log(result) // { value: 'kong_aaa_bbb_ccc', done: true }
console.log(generator.next()) // { value: undefined, done: true }
generator 对象中的 throw 方法
我们可以通过 generator 对象上的 throw 来抛出错误,可以在生成器中通过 try ... catch 来进行捕获
function* gen(prefix) {
console.log("函数执行~")
try {
yield "kong"
} catch (e) {
console.log("生成器函数内部捕获异常:", e) //
}
console.log("函数执行接收~")
}
const generator = gen("kong")
const res1 = generator.next()
generator.throw("error message")
/** ↑ log: ↓
函数执行~
生成器函数内部捕获异常: error message
函数执行接收~
*/
因为生成器是一种特殊的迭代器,那么在某些情况下我们可以使用生成器来替代迭代器(如前面可迭代对象中的 “类数组中的示例”)
const likeArray = {
0: 1,
1: 2,
2: 3,
length: 3, // -- 数据内部实现了迭代的方法,我们可以自己进行元编程来实现相应的方法
// [Symbol.iterator]() {
// let idx = 0
// return {
// next: () => {
// return { value: this[idx], done: idx++ === this.length }
// }
// }
// }
// -- 因为生成器函数返回的是一个迭代器对象,所以上面的方式我们也可以通过生成器函数来进行实现
*[Symbol.iterator]() {
let [idx, len] = [0, this.length]
while (idx !== len) {
yield this[idx++]
}
}
}
console.log([...likeArray])
yield 语法糖:*
该语法糖可以可以依次迭代一个可迭代对象,每次迭代其中的一个值(如 ↓)
function* createArrayIterator(arr) {
yield* arr
}
// -- 等价于
function* createArrayIterator(arr) {
for (const item of arr) {
yield item
}
}
异步处理方案
我们这里通过一个案例来一步一步的查看异步处理方案的过渡
例子: 我们模拟向服务器发送 3 次请求,且除第一个请求外的每一个请求都依赖上一个请求的结果 ↓
function requestData(arg) { // -- 模拟请求: 请求方法
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(arg)
}, 1000)
})
}
回调地狱
function getData() { // -- 模拟请求: 请求处理
requestData("kong").then(res1 => {
requestData(res1 + "_deng").then(res2 => {
requestData(res2 + "_wang").then(res3 => {
console.log(res3)
})
})
})
}
可以通过 Promise 中 then 的链式调用来解决该回调地狱问题
function getData() {
requestData("kong")
.then(res1 => requestData(res1 + "_deng"))
.then(res2 => requestData(res2 + "_wang"))
.then(res3 => {
console.log(res3)
})
}
虽然这种链式调用已经帮我们解决了回调地狱的问题,但上面的代码阅读性还是比较差的
我们可以通过 Generator 生成器还进行优化,通过生成器函数我们可以灵活的控制代码的执行与暂停(异步转同步)
Generator 生成器
function* getData() {
const res1 = yield requestData("kong")
const res2 = yield requestData(res1 + "_deng")
const res3 = yield requestData(res2 + "_wang")
console.log(res3)
}
+ 可以看出如果通过生成器函数来处理异步代码,在生成器函数中可以像同步的一样来进行处理,代码就更加简洁、阅读性高了
+ 不过下面 generator 对象处理部分就会显得特别不好阅读(不过它们都是有规律的,我们可以统一封装成一个工具函数来对该生成器进行处理,或借助一个 co 库)
// -- 不过生成器函数返回的是一个 generator 对象,我们还需要通过该 generator 来控制代码的执行与暂停操作
const generator = getData()
const { value, done } = generator.next()
if (!done) {
value.then(res1 => {
const { value, done } = generator.next(res1)
if (!done) {
value.then(res2 => {
const { value, done } = generator.next(res2)
if (!done) {
value.then(res3 => {
const { value, done } = generator.next(res3)
})
}
})
}
})
}
Generator + co : 自动处理生成器函数
co: 该库就是早期用于处理生成器函数的一个工具函数,如下示例
function* getData() {
const res1 = yield requestData("kong")
const res2 = yield requestData(res1 + "_deng")
const res3 = yield requestData(res2 + "_wang")
console.log(res3) // kong_deng_wang
return res3
}
+ npm i co
const co = require("co")
+ 该工具函数可以自动帮我们来处理生成器函数
co(getData).then(res => { // -- getData return 的结果
console.log(res) // kong_deng_wang
})
我们也可以根据处理生成器函数代码的规律来自己写一个 co 处理函数
function co(generatorFn) {
return new Promise((resolve, reject) => { // -- 该工具函数最终返回一个 Promise 对象
const generator = generatorFn()
function next(nextArg) { // -- 定义一个递归方法
const { value, done } = generator.next(nextArg)
if (!done) { // -- 当生成器还未迭代结束时,继续调用 next 进行迭代(如果存在异步代码,先等待异步代码的执行)
// -- 通过 Promise.resolve 来避免,当 value 不是 promise 时,无法通过 then 来进行判断... → 即: 当 vlaue 不是 promise 时,先将其转换成 promise 先
Promise.resolve(value).then(res => {
next(res) // -- 递归迭代,并将 data 传入上一次的 yeild 返回值中
}, (e) => generator.throw(e)) // -- 错误处理(可在生成器函数中通过 try ... catch 进行捕获)
}
else resolve(value) // -- 如果 done 为 true: 即已完成 → 兑现最终的结果
}
next(undefined)
})
}
+ TEST
co(getData).then(res => {
console.log(res) // kong_deng_wang
})
上面虽然说通过 Generator + co 来处理异步代码时,可以有比较好的处理,但是每次都需要引入一下 co 库与一些使用,还是稍微有那么一点麻烦
在 ECMAScript 2017(ES8)中就为我们提供了 async/await 关键字(语法糖),可以使我们能更好的处理异步代码(也可以理解为: async/await === Generator + co)
async/await 就相当于是 generator + co 的语法糖
它们允许我们以更接近于同步代码的方式来编写异步代码,从而提高代码的可读性和可维护性
基本使用
1. async 关键字用于声明一个异步函数,这意味者函数内部可以使用 await 关键字
2. await 关键字
await 关键字后面需要跟上一个表达式,这个表达式会返回一个 Promise 对象
awiat 关键字使代码暂停执行,会等待所返回的 Promise 对象变成 fulfilled 状态,才会继续执行后续的代码
await 关键字后面所跟的值,同样分为前面 Promise 中的 resolve 参数的三种情况
如果 await 后面是一个普通的值,那么会直接返回这个值
如果 await 后面是一个普通的值,那么会直接返回这个值
如果 await 后面的表达式,返回的 Promise 是 reject 的状态,那么会将这个 reject 结果直接作为函数的 Promise 的 reject 值
3. 异步函数的返回值
异步函数的返回值相当于被包裹 到Promise.resolve 中 → 该返回值与生成器函数中的返回值同理(也分为前面的三种情况)
如果返回的是一个普通的值,那么会直接返回这个值
如果我们的异步函数的返回值是 Promise,状态由会由 Promise 决定
如果我们的异步函数的返回值是一个对象并且实现了 thenable,那么会由对象的 then 方法来决定
4. 使用例子(我们以上面 Generator + co 的例子通过 async/awiat 的方式做一个示例)
+ Generator + co 方式
const co = require("co")
function* getData() {
const res1 = yield requestData("kong")
const res2 = yield requestData(res1 + "_deng")
const res3 = yield requestData(res2 + "_wang")
console.log(res3)
return res3
}
co(getData).then(res => {
console.log(res) // kong_deng_wang
})
+ async/await 示例
async function getData() { // -- 通过 async 关键字,声明该函数是一个异步函数
const res1 = await requestData("kong") // -- 通过 await 关键字,使其异步函数转成同步执行
const res2 = await requestData(res1 + "_deng")
const res3 = await requestData(res2 + "_wang")
console.log(res3)
return res3
}
getData().then(res => {
console.log(res) // kong_deng_wang
})
该两种方式可以理解为几乎是等价的(async/await 就相当于是 Generator + co 的一种语法糖)
1. Promise 出现是为了解决异步回调的问题(通过链式调用)
2. Iterator 与 Generator 的出现是为了解决 Promise 过多的链式调用代码阅读性不友好的问题,但使用通常需要借助一个工具函数来对生成器函数进行处理(通过代码的暂停执行与继续执行 - 异步转同步)
3. async/await 是异步编程的一个语法糖,相当于 Generator + co 工具函数的一个结合,使其可以在使用时,不需要每次都引入相应的工具函数,与使用起来更见的方便(Generator + co 的语法糖)
4. 基本过渡流程: 回调地狱 → Promise → Generator → async/await