直击主题
js
function* say() {
console.log('start');
let r1 = yield 'hello';
console.log(r1);
let r2 = yield 'world';
console.log(r2);
return true;
}
let iSay = say();
// 输出
// 空,无输出
console.log(iSay.next(1));
// 输出
// start
// { value: 'hello', done: false }
console.log(iSay.next(2));
// 输出
// 2
// { value: 'world', done: false }
console.log(iSay.next(3));
// 输出
// 3
// { value: true, done: true }
console.log(iSay.next()); // { value: true, done: true }
say 就是一个 generator function(生成器函数),调用它就会返回一个 generator(生成器)。生成器有一个 next 方法,每次调用会返回一个对象。该对象有两个属性,value 和 done,value 为生成器函数中 yield 的返回值,done 表示生成器函数是否执行完成。
生成器函数执行后会返回一个生成器对象,但是此时生成器函数中的代码不会执行,直到生成器对象调用 next()。
生成器对象调用 next 方法会执行生成器函数中代码到下一个(或第一个) yield 处, 此时生成器函数的执行暂停,直到生成器对象调用 next。
调用 next 可以传参,本次 next 调用会从暂停处 yield 处开始运行,暂停处的 yield 的返回值即是 next 的传参。
由于第一次暂停是调用生成器函数时发生的,所以第一个 next(1) 不会从某个 yield 暂停处开始继续执行,所以第一个 next 传参无 yield 进行返回。可以理解为第一次 next 调用时没有暂停的 yield,所以没有 yield 来承接第一个 next 传参。
那这样的函数有什么用呢?它能做什么? 简单分析,可以看出它有一个非常有趣的特征,就是生成器函数不会一调用就从头执行到尾,而是受调用方驱动。调用方执行一下 next,函数往下走一步(走到下个 yield)。需要不停的调用 next 直到整个函数执行完成。
了解到了这个运行特征,它就会让代码变得很有趣了。比如解决个异步回调潜逃问题: 假设有个代码是这么写的:
js
function getData(onSucceed){
http.request('https://xx.xx.com/data',function(result){
onSucceed(result);
});
}
function getUser(onSucceed){
http.request('https://xx.xx.com/user',function(result){
onSucceed(result);
});
}
function showUserData(){
getData(function(data){
getUser(function(user){
var userData = data.filter(d=>d.user_name===use.name);
console.log('展示用户数据',userData);
})
})
}
众所周知嵌套层级多了,将导致代码非常难维护。那通过生成器函数如何解决这个问题呢?也许你已经想到了:
js
// 获取到生成器对象
var gShowUserData = showUserData();
// 启动生成器函数,运行到 let data = yield getData() 的 yield 处, getData() 会执行
gShowUserData.next();
function getData(){
http.request('https://xx.xx.com/data',function(result){
// next 会启动生成器函数运行到下一个 yield, 也就是 let user = yield getUser(), getUser () 会执行并暂停在此处。 getUser() 会执行。
// 本次 next 传参数会被 let data = yield 的 yield 返回
gShowUserData.next(result);
});
}
function getUser(){
http.request('https://xx.xx.com/user',function(result){
// next 会启动生成器函数运行到最后(后面没有 yield 了,所以不会再暂停了)
// next 传参数会被 let user = yield 处的 yield 返回
gShowUserData.next(result);
});
}
function* showUserData(){
let data = yield getData();
let user = yield getUser();
var userData = data.filter(d=>d.user_name===user.name);
console.log('展示用户数据',userData);
}
简单解释下代码的核心思想,就是将回调驱动后续流程的代码变为了,异步回调驱动生成器函数继续往下执行,异步回调执行之前生成器函数是暂停的,不会继续执行,这样生成器函数就可以等到数据都获取完成了再执行最后的数据处理的代码。
那么你也许会说,现在 promise 通过链式调用也能解决嵌套的问题,async、await 也类同步代码能解决嵌套的问题。没错,那有没有发现刚写的 showUserData 和 async 和 await 很像,如果你把 * 想象为 async、 把 yield 想象为 await 的话。你可以理解为 async、await 就是生成器函数的语法糖。
生成器函数 yield 的另一个特性:yield*。 yield* 会自动将 function* 进行解构,举个简单例子:
js
function* say1(){
yiled 1;
yiled 2;
}
function* say2(){
yield 4;
yield 5;
yield 6;
}
funciton* say(){
yield* say1();
yield 3;
yield* say2();
}
这个例子里 say 函数是等价于:
js
funciton* say(){
yiled 1;
yiled 2;
yield 3;
yield 4;
yield 5;
yield 6;
}
因为 yield* 会自动完成对生成器的解构。
有了以上知识,我们来尝试用生成器函数来翻译一下一个 async、await 的函数实现
js
function async getData(){
var data = await Promise.resolve([{name:1},{name:2}]);
return data;
}
function async getUser(){
var data = await Promise.resolve({name:1});
return data;
}
function async showUserData(){
var data = await getData();
var user = await getUser();
var userData = data.filter(d=>d.user_name===user.name);
console.log('展示用户数据',userData);
}
按之前的设想我们可以直接把代码翻译为这样:
js
function* getData(){
var data = yield Promise.resolve([{name:1},{name:2}]);
return data;
}
function* getUser(){
var data = yield Promise.resolve({name:1});
return data;
}
function* showUserData(){
var data = yield* getData();
var user = yield* getUser();
var userData = data.filter(d=>d.user_name===user.name);
console.log('展示用户数据',userData);
}
根据 yield* 的逻辑我们再翻译为这样:
js
function* showUserData(){
var data_getData = yield Promise.resolve([{name:1},{name:2}]);
var data = data_getData;
var data_getUser = yield Promise.resolve({name:1});
var user = data_getUser;
var userData = data.filter(d=>d.user_name===user.name);
console.log('展示用户数据',userData);
return '执行完成';
}
现在我们只要能让最后这个函数能跑起来就行了,但是目前来看显然是不行的,因为 yield 返回的是 promise,我们需要将生成器自动执行完。执行逻辑就是获取 yield 返回值为 promise 的对象的决议值之后再通过 next 传入决议值启动生成器继续往下执行,直到生成器函数执行完成。
js
function runGenerator(generatorObj) {
return new Promise((resolve, reject) => {
function next(data) {
try {
// 通过生成器对象执行到生成器函数的最终
var { value, done } = generatorObj.next(data);
}catch(e){
return reject(e);
}
if (!done) {
// done 为 true,表示迭代完成
// value 不一定是 Promise,可能是一个普通值。使用 Promise.resolve 进行包装。
Promise.resolve(value).then(val => {
next(val);
}, reject);
} else {
// 最终值
resolve(value);
}
}
next(); // 启动生成器函数的执行
});
}
var gObj = showUserData();
runGenerator(gObj).then(data => {
console.log(data);// 执行完成
}).catch((err) => {
console.log('err: ', err);
});
就这样我们完成了一次对 async、await 函数的翻译。
总结
本文本主要介绍生成器函数的一些特点,以及与 async、await 的关系。当然,关于生成器的知识还有很多,就不一一展开了,比如与 Iterator 的关系(for of 依赖)也是比较有意思的。