在 ES6 中提供了一种新的异步编程解决方案,那就是 Generator 函数。这种函数又被称为生成器函数、可暂停函数。在它的方法体内可以使用 yield 关键字,配合 yield 可以实现协程。
本文主要介绍对 Generator 函数的实际应用,不过多介绍 Generator 函数的相关基础概念。
const axios = require('axios')
axios.get('https://sxyz.blog').then((res) => {
console.log(res.data)
})
上面代码中使用了 axios 模块发起了一个简单的 GET 请求,当这个请求得到响应时会执行 then 块中的代码,打印出响应正文。
但是如果出现连续操作(请求完一个后接着请求下一个)的时候,那么就要这么写了:
const axios = require('axios')
axios.get('https://sxyz.blog').then((res) => {
axios.get('https://sxyz.blog').then((res) => {
console.log(res.data)
})
})
从上面代码中可以发现,如果当这种连续请求的操作变得多了起来,那么就会出现 then 连环嵌套的尴尬场面。而这种需求在日常编码中却又非常常见。
const axios = require('axios')
function* request() {
let res
res = yield axios.get('https://sxyz.blog')
console.log(res.data)
res = yield axios.get('https://sxyz.blog')
console.log(res.data)
}
let p1, p2
const r = request()
p1 = r.next().value // Promise object
p1.then((v) => {
p2 = r.next(v).value
p2.then((v) => {
r.next(v)
// p3 = r.next(v).value
// p3.then(...)
// ...
})
})
上面代码中,创建了一个名为 request
的 Generator 函数,然后在后面手动调用 next
方法接收 axios.get
所产生的 Promise 对象,并且调用 then 方法依次执行。
虽然现在我们的关键代码(request 函数体中的请求代码)已经是同步了,这很棒。但是还有一个问题就是我们需要在代码中手动依次调用 next
来完成这些操作,这个过程非常繁琐且不合理。并且 then 的嵌套问题依旧存在。所以我们需要封装一个函数来帮我们自动完成这些操作:
const axios = require('axios')
function* request() {
let res
res = yield axios.get('https://sxyz.blog')
console.log(res.data)
res = yield axios.get('https://sxyz.blog')
console.log(res.data)
}
const co = (gen, ...args) => {
const g = gen(...args)
const actuator = (x) => {
if (x.done) {
return
}
x.value.then((value) => {
actuator(g.next(value))
})
}
actuator(g.next())
}
// 现在只需
co(request)
// 如果有参数
co(request, 'aaa', 'bbbb')
其中 co 函数用来自动触发所有的 next
方法,并且解决了 then 不停嵌套的问题。
那么现在还有一些问题,比如我该如何得到 Generator 函数所产生的返回值呢,例如下面这样的代码:
const axios = require('axios')
function* foo() {
return yield axios.get('https://sxyz.blog')
}
function* bar() {
const { data } = yield foo()
console.log(data)
return yield axios.get('https://sxyz.blog')
}
co(
(function* () {
console.log((yield bar()).data)
})()
)
想要在 co 的匿名函数中得到 bar 的执行结果,同时 bar 函数也可以得到 foo 的函数结果。
如果要实现这样的功能,需要对 co 函数进行一些修改:
const isGenerator = (f) => f.constructor && f.constructor.name === 'GeneratorFunction'
const isGeneratorObject = (o) => o.constructor && o.constructor.constructor && o.constructor.constructor.name === 'GeneratorFunction'
const co = (g) => {
let resolve
const p = new Promise((r) => (resolve = r))
const actuator = (x) => {
if (x.done) {
return resolve(x.value)
}
Promise.resolve(isGeneratorObject(x.value) ? co(x.value) : x.value).then(
(value) => {
actuator(g.next(value))
},
(reason) => {
g.throw(reason)
}
)
}
actuator(g.next())
return p
}
现在统一了每个 co 函数都返回 Promise 对象。加进了对 Generator 对象的检查,如若 yield 后面的不是 Promise 对象而是 Generator 对象时,则再次调用 co 自身来转成 Promise 对象。
我们将上面请求的代码进一步封装成一个类:
class Test {
*foo() {
return yield axios.get('https://sxyz.blog')
}
*bar() {
const { data } = yield this.foo()
console.log(data)
return yield axios.get('https://sxyz.blog')
}
}
co(
(function* () {
const t = new Test()
console.log((yield t.bar()).data)
})()
)
但是此时发现,所有与类有关的操作都需要写到 co 的函数体内,有点麻烦,有没有办法可以直接使用呢?有的,可以使用 ES6 中的 Proxy 对象来实现:
const get = (target, prop, receiver) => {
return (...args) => {
if (!prop in target) {
return
}
return isGenerator(target[prop]) ? co(target[prop].call(target, ...args)) : target[prop].call(target, ...args)
}
}
const te = (c) =>
new Proxy(c, {
construct(cls, args) {
return new Proxy(new cls(...args), { get })
},
})
const Test = te(
class {
*foo() {
return yield axios.get('https://sxyz.blog')
}
*bar() {
const { data } = yield this.foo()
console.log(data)
return yield axios.get('https://sxyz.blog')
}
}
)
const t = new Test()
t.bar().then((res) => {
console.log(res.data)
})
这样就可以不用再单独写 co 函数了,不过可能实际用途不是很大,参考只用。
本文介绍了如何使用 Generator 函数实现将异步操作转变为同步操作的方法。但在实际编码过程中可直接使用 await/async 语法糖。本文算是作为补充 Generator 同步实现的原理来看吧。