什么是 Generator 函数
Generator 函数是 ES6 提供的一种异步编程解决方案(可以按序执行异步方法),但其语法行为与传统函数完全不同。
先看看 Generator 函数在形式上的定义:
let log = console.log
function* gen() {
yield 1
yield 2
yield 3
return 4
}
let g = gen()
log(g.next()) // { value: 1, done: false }
log(g.next()) // { value: 2, done: false }
log(g.next()) // { value: 3, done: false }
log(g.next()) // { value: 4, done: true }
log(g.next()) // { value: undefined, done: true }
- generator 函数和普通函数不同的是,generator 由 function*定义(function后带有一个星号 *)
- yield 表达式:你可以理解为 Generator 函数是一个状态机,封装了多个内部状态,而函数体内部使用 yield 表达式来定义不同的内部状态
- 执行 Generator 函数后会返回一个遍历器对象(不会直接得到 return 的结果)
- 依次调用遍历器对象的 next 方法,可以遍历 Generator 函数内部的每一个状态
继续分析上面的代码,如果一个 yield 表达式算一个 generator 的一个状态,上述的代码一共有4个状态,即 3 个 yield 表达式和 1 个 return 语句(return 也算一个状态)。
每次调用遍历器的 next 方法,都会返回一个对象 {value: xxx, done: xxx}
,表示当前遍历器状态的信息,该对象包含两个属性,一个是 value 属性,表示当前 yield 表达式的值, 一个是 done 属性,表示当前遍历是否结束。当所有状态遍历结束后,done 的值变为 true。
当遍历结束后如果继续调用遍历器的 next 方法,done 的值不再改变,而 value 的值变为 undefined。
yield 表达式
- yield 语句就是暂停标志,遇到 yield 语句就暂停执行后面的操作,并将紧跟在 yield 后的表达式的值作为返回的对象的 value 属性值。
- 下一次调用next 方法时再继续往下执行,知道遇到下一条 yield。
- 如果没有再遇到新的 yield 语句,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值作为返回对象的 value 属性值。
- 如果该函数没有 return 语句,则返回对象的 value 属性值为 undefined。
不能在其他普通函数体中使用 yield,会报语法错误
(function (){
yield 1;
})() // SyntaxError: Unexpected number
yield 表达式如果用在另一个表达式中,必须放在圆括号里面
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
next 方法
注意,yield 语句本身没有返回值,或者说总是返回 undefined。next 方法可以带有一个参数,该参数会被当作上一条 yield 语句的返回值。
因为上面这句话,也就有了下面这个经典的例子:
function* foo(x) {
let y = 2 * (yield(x + 1))
let z = yield(y / 3)
return (x + y + z)
}
let g = foo(5)
log(g.next())
log(g.next())
log(g.next())
猜猜打印的结果什么?答案如下:
{ value: 6, done: false }
{ value: NaN, done: false }
{ value: NaN, done: true }
第一个 next() 语句中的参数总是无效的,无论传入什么值,都不会对接下来的表达式产生影响,因为在执行第一个 next() 方法的时候,还没遇到 yield 语句,参数也无法作为返回值,所以第一个next的参数是无意义的。
可能有点绕,按步骤说明下上述代码的执行过程:
g = foo(), foo 生成了遍历器,返回给了 g 变量
执行第一个,g.next(),g 执行 next 方法,此时在代码中体现为执行了 x + 1 = 6
然后遇到了 yield 语句,暂停,返回结果 {value: 6, done: false}
执行第二个,g.next(), next 参数为空,即默认上一个yield语句的返回值为 undefined,执行 let y = 2 * undefind, y = NaN, 继续计算 y/3 , 即 NaN/3 = NaN
然后遇到了 yield 语句,暂停,返回结果 {value: NaN, done: false}
执行第三个,g.next(), next 参数为空,即默认上一个yield语句的返回值为 undefined, 执行 let z = undefined, return (x+y+z) 即 return (5+NaN+undefined) 最后的返回值 undefined。
为 next 方法传入一些参数:
function* foo(x) {
let y = 2 * (yield(x + 1))
let z = yield(y / 3)
return (x + y + z)
}
let g = foo(5)
log(g.next()) // { value:6, done:false }
log(g.next(12)) // { value:8, done:false }
log(g.next(13)) // { value:42, done:true }
可以按上述的过程走一遍,然后把参数代入就得到了注释中的结果。
for...of 循环
for...of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
上面代码使用for...of循环,依次显示 5 个yield表达式的值。这里需要注意,一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。
Generator.prototype.throw()
Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b
上面代码中,遍历器对象i连续抛出两个错误。第一个错误被 Generator 函数体内的catch语句捕获。i第二次抛出错误,由于 Generator 函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的catch语句捕获。
throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例。
var g = function* () {
try {
yield;
} catch (e) {
console.log(e);
}
};
var i = g();
i.next();
i.throw(new Error('出错了!'));
// Error: 出错了!(…)
注意,不要混淆遍历器对象的throw方法和全局的throw命令。上面代码的错误,是用遍历器对象的throw方法抛出的,而不是用throw命令抛出的。后者只能被函数体外的catch语句捕获。
利用 Genertor 函数返回的迭代器的 throw 方法可以很好地处理代码运行中的错误,这点在实现一个具有错误处理的 async 函数中也有一定体现。
参考文章:
[1] https://www.cnblogs.com/rogerwu/p/10764046.html
[2] https://es6.ruanyifeng.com/#docs/generator
async 和 await 的简单实现
async/await 被称为是 generator 的语法糖,实际上 async/await 就是 generator 函数加上自动执行器来实现的。
从上一小节知道,generator 函数是不会自动执行的,每一次调用它的 next 方法,会停留在下一个 yield 的位置。
利用这个特性,我们只要编写一个自动执行的函数,就可以让这个 generator 函数完全实现 async 函数的功能。
先看一个 async 函数的示例
let p = function (val) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(val)
}, 1000);
})
}
async function testAsync() {
const data1 = await p(1)
console.log(data1)
const data2 = await p(2)
console.log(data2)
const data3 = await p(3)
console.log(data3)
}
testAsync()
这个 async 函数的效果就是,每隔1秒,按序打印出 1,2,3
尝试利用 generator 函数替代 async 函数来实现这段代码。
function* testG() {
const data1 = yield p(1)
console.log(data1)
const data2 = yield p(2)
console.log(data2)
const data3 = yield p(3)
console.log(data3)
}
let gen = testG()
let dataPromise = gen.next().value // 返回对象中的 value 值才是一个 promise
dataPromise.then((val) => {
let data2Promise = gen.next(val).value
data2Promise.then((val2) => {
let data3Promise = gen.next(val2).value
data3Promise.then((val3) => {
gen.next(val3)
})
})
})
// 按序每隔一秒打印 1、2、3
为了保证每个状态获取到正确的值,并且按序执行,则 gen.next() 必须在 promise 对象的 then 方法的回调中执行,以保证在 generator 的 next 方法中传入正确的参数,当有多个异步方法要执行的时候,最终实现的效果就像一个回调地狱的调用。
不过好歹是用 generator 实现了 async 函数的效果,尽管代码不是很优美。
实现一个高阶函数来代替回调地狱
先不考虑包含错误处理的情况:
function asyncToGenerator(generatorFunc) {
return function () {
// 相当于 gen = generatorFunc() ,顺便引入上下文环境,返回一个遍历器
const gen = generatorFunc.apply(this, arguments)
return new Promise((resolve) => {
// 定义一个步进函数,递归调用,直到遍历器的返回结果 done = true
function step(arg) {
let generatorResult = gen.next(arg)
const {
value,
done
} = generatorResult
if (done) {
return resolve(value)
} else {
// 不一定每一个返回值都是 promise 函数,这里需要用 Promise.resolve 方法来包装一下。
return Promise.resolve(value).then(val => step(val))
}
}
step() // 默认参数是 undefined,所以第一次执行这里不传参
})
}
}
asyncToGenerator(testG)()
考虑错误处理的情况,加逐行解释代码
参考:https://juejin.im/post/6844904102053281806
function asyncToGenerator(generatorFunc) {
// 返回的是一个新的函数
return function() {
// 先调用generator函数 生成迭代器
// 对应 var gen = testG()
const gen = generatorFunc.apply(this, arguments)
// 返回一个promise 因为外部是用.then的方式 或者await的方式去使用这个函数的返回值的
// var test = asyncToGenerator(testG)
// test().then(res => console.log(res))
return new Promise((resolve, reject) => {
// 内部定义一个step函数 用来一步一步的跨过yield的阻碍
// key有next和throw两种取值,分别对应了gen的next和throw方法
// arg参数则是用来把promise resolve出来的值交给下一个yield
function step(key, arg) {
let generatorResult
// 这个方法需要包裹在try catch中
// 如果报错了 就把promise给reject掉 外部通过.catch可以获取到错误
try {
generatorResult = gen[key](arg)
} catch (error) {
return reject(error)
}
// gen.next() 得到的结果是一个 { value, done } 的结构
const { value, done } = generatorResult
if (done) {
// 如果已经完成了 就直接resolve这个promise
// 这个done是在最后一次调用next后才会为true
// 以本文的例子来说 此时的结果是 { done: true, value: 'success' }
// 这个value也就是generator函数最后的返回值
return resolve(value)
} else {
// 除了最后结束的时候外,每次调用gen.next()
// 其实是返回 { value: Promise, done: false } 的结构,
// 这里要注意的是Promise.resolve可以接受一个promise为参数
// 并且这个promise参数被resolve的时候,这个then才会被调用
return Promise.resolve(
// 这个value对应的是yield后面的promise
value
).then(
// value这个promise被resove的时候,就会执行next
// 并且只要done不是true的时候 就会递归的往下解开promise
// 对应gen.next().value.then(value => {
// gen.next(value).value.then(value2 => {
// gen.next()
//
// // 此时done为true了 整个promise被resolve了
// // 最外部的test().then(res => console.log(res))的then就开始执行了
// })
// })
function onResolve(val) {
step("next", val)
},
// 如果promise被reject了 就再次进入step函数
// 不同的是,这次的try catch中调用的是gen.throw(err)
// 那么自然就被catch到 然后把promise给reject掉啦
function onReject(err) {
step("throw", err)
},
)
}
}
step("next")
})
}
}