生成器 Generator
简介
Promise的出现,我们可以将回调进行反客为主,不在受回调调用者的限制(控制反转和信任问题),但是它并没有解决异步编程导致的代码顺序问题,有没有一种方式,可以让我们的代码虽然是异步的,但是书写顺序却是同步的,这样完全符合我们人类大脑理解方式?
console.log("同步的1");
axios({ ...
}).then(() => {
console.log("异步的回调,如果需要等待结果处理,代码只能写在这个回调里")
});
console.log("同步的2")
很明显,如果我们希望"同步的2"
在请求之后打印,只能写在then的回调函数中,但是这显然不是最佳的可被大脑阅读的代码,特别是在一些人根本不知道axios是什么的时候。
他很有可能会认为then中的log打印是在"同步的2"
之前输出的。
Generator 生成器
在我们的认知里,当一个函数运行时,它一定会完整运行完才会运行下一个函数。
一个函数一旦开始执行,就会运行到结束,期间不会有其他代码能够打断它并插人其间
function a() {
//完整运行完后
}
function b() {
//才会到这里
}
a();
b();
这个特性也是因为我们的JavaScript是单线程语言。
在ES6的时候引入了一个新的函数类型:生成器(Generator);它并不符合我们上述的这种特性,它可以通过yield
实现暂停,但是它只是暂停函数内部的运行,并不会影响到函数外的代码,生成器运行后会生成一个迭代器,外部通过运行迭代器的next
方法,实现结束暂停继续运行,直到下一个yield
或者函数运行结束。
var x = 1;
function* foo() {
x++;
yield; // 暂停
console.log("x:", x);
}
可以看到foo
函数的前面是带有*
号的,当然不止这种写法,还有好几种写法,其实都是一个意思:
function* foo(x, y) {···}
function* foo(x, y) {···}
function* foo(x, y) {···}
function* foo(x, y) {···}
其中的yield
表示暂停,暂停执行后面的代码,这个后面指的是yield ...;
的后面,也就是说yield后面是可以接值的,这个值或者说表达式并不会被暂停,而是值后面的代码会被暂停。
yield 后面的值会作为迭代器对象的value属性的值返回出去。
function* foo() {
yield 1;
console.log(2);
yield 3;
}
const it = foo(); //运行生成器生成迭代器实例
const res1 = it.next(); //运行一次
console.log(res1.value); //1
const res2 = it.next(); //运行一次,log输出2
console.log(res2.value); //3
运行第一次next的时候,foo生成器中的代码才是真正的运行,而foo()
只是生成迭代器实例,并不会运行里面的代码。
第一次next运行到一个yield
处,此时后面跟着一个值1,这个值会被赋值给迭代器对象value属性,我们在外部可以获取到这个迭代器,从而获取到1
。
此时后面的代码是暂停的。
当我们运行第二次next的时候,log打印出2
;然后在第二个yield
处停止,yield 后面跟着一个3,3会被赋值给迭代器对象value属性,外面通过value获取到这个值。
但是需要注意,迭代器并没有停止,或者说迭代完毕,我们打印res3
得到:
{
value: 3,
done: false
}
done不等于true
表示还没有迭代完成,我们再运行一次next,此时才算迭代完成。
const res3 = it.next(); //运行一次
console.log(res3); //{ value: undefined, done: true }
此时没有yield,函数默认结尾是 return undefined
,于是这个值被作为迭代器value的值,此时done被改为true。
手动结束
迭代器是支持手动结束的,返回的迭代器对象it
除了next还有两个属性方法:
return()
:返回给定的值,并且终结遍历 Generator 函数throw()
:往 Generator 函数体内传入一个错误对象,throw new Error()这种
这两个方法,其中return是可以直接中介循环,而throw是类似于抛出错误的形式,从而结束代码,但是如果Generator 函数内部try...catch了,如果错误被try捕获了,那么就无法结束了。
function* foo() {
yield 1;
console.log(2);
yield 3;
}
const it = foo(); //运行生成器生成迭代器实例
const res1 = it.throw("错误"); //运行一次 Uncaught 错误
console.log(1111); //运行不到这了
const res2 = it.next(); //运行不到这了
console.log(res2); //运行不到这了
捕获了错误:
function* foo() {
try {
yield 1;
console.log(2);
yield 3;
} catch (error) {
console.log("捕获了错误", error);
}
yield 4;
}
const it = foo(); //运行生成器生成迭代器实例
const res1 = it.next(); //运行一次
console.log(res1);
const res2 = it.throw("错误"); //捕获了错误 错误
console.log(res2); //{ value: 4, done: false }
const res3 = it.next(); //运行一次
console.log(res3); //{ value: undefined, done: true }
由于捕获了错误,所以代码还是会继续运行,于是运行到了yield 4;
,所以打印res2时value是4。
return()则相对好理解一些。
function* foo() {
yield 1;
console.log(2);
yield 3;
}
const it = foo(); //运行生成器生成迭代器实例
const res1 = it.next(); //运行一次
console.log(res1.value); //1
const res2 = it.return("结束"); //运行一次
console.log(res2); // { value: "结束", done: true }
const res3 = it.next(); //运行一次
console.log(res3); //{ value: undefined, done: true }
next也可以传值
事实上生成器生成的迭代器在调用其next方法的时候,也是可以传值的,这个值可以看成是运行到的yield xxx;
代码的结果。但是第一次的next不管传什么都会被忽略,因为第一次是启动迭代并运行到yield处,第二次next时传的值才是第一个yield的结果。
function* foo() {
try {
const a = yield 1;
console.log(111, a); //111 undefined
console.log(2); // 2
yield 3;
} catch (error) {
console.log("捕获了错误", error);
}
yield 4;
}
const it = foo(); //运行生成器生成迭代器实例
const res1 = it.next("第一次"); //运行一次
console.log(res1); //{ value: 1, done: false }
const res2 = it.next(); //运行一次
console.log(res2); //{ value: 3, done: false }
const res3 = it.next(); //运行一次
console.log(res3); //{ value: undefined, done: true }
打印的顺序:
//{ value: 1, done: false }
//111 undefined
//2
//{ value: 3, done: false }
//{ value: 4, done: false }
第一个next的参数被忽略,无人使用它,传了也会被丢弃,因为规范就是这样。
异步
我们可以发现,只有调用了next方法,代码才会往下运行,这就带来一个非常大的改变,我们可以将异步的处理抽出来,然后在异步的回调里运行next方法,从而可以实现代码的同步书写顺序。
function foo(x, y) {
axios(`xxx?x=${x}&y=${y}`)
.then(function(res) {
it.next(data);
})
.catch(function(err) {
it.throw(err);
});
}
function* main() {
try {
var text = yield foo(11, 31);
console.log(text);
} catch (err) {
console.error(err);
}
}
var it = main();
// 启动
it.next();
这个时候你会发现,main里面的代码其实是同步运行的。
但是会有一个问题,我们需要手动next
触发,而且如果代码存在多个 yield
就得运行多个next方法,显然这并不是很方便。
运行器
如果能有一个工具,能够帮我们省略掉书写next运行代码就好了,事实上也是有的,但是由于这些库昙花一现,就不去找具体的链接了,我们可以看一个示例代码:
function run(gen) {
var args = [].slice.call(arguments, 1),
it;
it = gen.apply(this, args);
return Promise.resolve().then(function handleNext(value) {
var next = it.next(value);
return (function handleResult(next) {
if (next.done) {
return next.value;
} else {
return Promise.resolve(next.value).then(handleNext, function handleErr(err) {
return Promise.resolve(it.throw(err)).then(handleResult);
});
}
})(next);
});
}
function* main() {
const a = yield Promise.resolve(1);
console.log(a);
const b = yield Promise.resolve(2);
console.log(b);
}
run(main);
这段代码结合了promise,整体会稍微复杂一些,但是一定要读懂。
其实原理也非常简单,通过命名函数的方式实现递归调用,通过promise协议,在then之后将结果传给handleNext
函数,函数判断迭代器是否已经结束,如果结束直接return出结果。
如果没有结束,则继续handleNext
运行,在函数内部通过next方法得到迭代器,将迭代器的值封装到一个新的promise中,继续等待结果,以此往复。
async ? await?
此时你会发现,这种方式和现在我们常用的es7定义的async await非常相似,其实这种用法就是我们在es6时自己实现的用法,由于这是一种非常强大的方法,于是被纳入了标准中完善。
所以async和await底层其实就是Generator 生成器。
生成器委托
事实上我们除了yield一些异步处理函数,我们可能还会有yield 生成器()
的需求,这在一些稍微复杂的场景也是非常常见的,一个请求需要等待前面好几个异步函数的结果,为了方便会将一些异步函数封装成一个api异步函数(生成器)调用并做yield 等待其结果。
而这种方式可以利用生成器委托实现,用法就是在yield 时在生成器函数前面加*
号。
yield *foo();
此时foo的迭代器会委托给外部的生成器处理,不需要手动next或者再套一个run函数包起来。
ES6之前的生成器
其实就是如果在没有生成器的环境使用生成器。
从写法上我们无法使用更加简洁的方式,所以如果需要达到这种效果,需要写一堆套路代码。
//request会返回promise
function* foo(url) {
try {
console.log("requesting:", url);
var val = yield request(url);
console.log(val);
} catch (err) {
console.log("Oops:", err);
return false;
}
}
var it = foo("http://some.url.1");
转换后:
function foo(url) {
//管理器生成状态
var state;
//生成器变量范围声明
var val;
function process(v) {
switch (state) {
case 1:
console.log("requesting:", url);
return request(url);
case 2:
val = v;
console.log(val);
return;
case 3:
var err = v;
console.log("Oops:", err);
return false;
}
}
//构造并返回一个生成器
return {
next: function(v) {
//初始状态
if (!state) {
state = 1;
return {
done: false,
value: process(),
};
}
//yield
else if (state == 1) {
state = 2;
return {
done: true,
value: process(v),
};
}
//生成器已经完成
else {
return {
done: true,
value: undefined,
};
}
},
throw: function(e) {
//唯一的显示错误处理在状态1
if (state == 1) {
state = 3;
return {
done: true,
value: process(e),
};
}
//否则错误不会处理,直接抛出
else {
throw e;
}
},
};
}
将代码拆分成步骤,通过状态值管理进度,通过调用封装的回调函数,接受进度值来触发对应的具体逻辑代码。
对外返回一个迭代器,暴露next和throw方法。
你会发现这其实是一个套路,但是具体的业务代码也得写在里面,这就导致如果使用了async和await,对于低版本的支持,如果手动兼容的话,那还不如不用async呢!
所以后续大佬们提供了自动转换的工具:regenerator
由facebook的几位大佬开发的,其实大体原理都是差不多的,只是工具帮你自动做了,所以咱们现在使用async await的时候,都没啥后顾之忧了。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据