生成器下(生成器异步)
生成器下(生成器异步)
上一章讨论了生成器作为一个产生值的机制的特性,但生成器远远不止这些,更多的时候我们关注的是生成器在异步编程中的使用
生成器 + Promise
简略的描述,生成器异步就是我们在生成器中yield出一个 Promise,然后在Promise完成的时候重新执行生成器的后续代码
function foo(x, y) {
return request(
"http://some.url?x=1y=2")
}
function *main() {
try {
const text = yield foo(11, 31);
console.log(text);
} catch (err) {
console.error( err );
}
}
const it = main();
const p = it.next().value;
p.then(function (text) {
it.next(text);
}, function (error) {
it.throw(error);
});
上述代码是一个生成器+Promise的例子, 要想驱动器我们的main生成器,只需要在步骤后then继续执行即可,这段代码有不足之处,就是无法自动的帮助我们去实现Promise驱动生成器,可以看到上面我还是手动的写then回调函数去执行生成器, 我们需要不管内部有多少个异步步骤,都可以顺序的执行, 而且不需要我们有几个步骤就写几个next这么麻烦,我们完全可以把这些逻辑隐藏于某个工具函数之内,请看下面的例子
//第一个参数是一个生成器,后续的参数是传递给生成器的
//返回一个Promise
//当返回的Promise决议的时候,生成器也就执行完成了
function run(gen, ...args) {
const it = gen.apply(this, args);
return Promise.resolve()
.then(function handleNext(value) {
const ans = it.next(value);//执行
return (function handleResult(ans) {
//ans是执行结果
if (ans.done) {
return ans.value;//执行完毕
}
//没有执行完毕,我们需要继续异步下去
return Promise.resolve(ans.value)
.then(handleNext, function handleError(err) {
return Promise.resolve(it.throw(err))
.then(handleResult);
});
})(ans);
})
}
下面的代码演示如何使用这个run函数,
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<img src="" alt="" data-action="show-dog1">
<img src="" alt="" data-action="show-dog2">
<img src="" alt="" data-action="show-dog3">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>//引入axios
<script src="./run.js"> //run.js内容就是上例中定义的函数
</script>
<script>
function *main () {
const {data: { message }} = yield axios.get("https://dog.ceo/api/breeds/image/random");//一个友爱的能获得狗狗图片链接的api网站, 可以访问其官网https://dog.ceo/dog-api/
document.querySelector("[data-action='show-dog1']").src = message;
const {data: { message: message2 }} = yield axios.get("https://dog.ceo/api/breeds/image/random");
document.querySelector("[data-action='show-dog2']").src =message2;
const {data: { message: message3 }} = yield axios.get("https://dog.ceo/api/breeds/image/random");
document.querySelector("[data-action='show-dog3']").src =message3;
}
try {
run(main)
.then((ans) => {
console.log("ans", ans); //这里接受生成器最后return的值,在该例中为undefined
});
} catch (err) {
console.log(err);
}
</script>
</body>
</html>
run会运行你的生成器,直到结束,这样我们在生成器中就统一了异步和同步,所有的代码都可以以顺序的步骤执行,而我们不必在于是异步还是同步,完全可以避免写异步回调代码,
Async Await
在ES8中引入了async, await,这意味着我们也不需要使用写生成异步和run了,Async Await顺序的代码格式避免回调异步带来的回调地狱,回调信任问题一系列问题,如果按时间描述js异步的发展,大概就是从回调异步时代 到Promise ,然后生成器被大神发掘出来了,发现Promise + 生成器 有c#的async/await的效果,js官方觉得这是一个很好的用法,所以在es8中出了async/await, async/await就是Promise + 生成器的语法糖,可以认为promise + 生成器是其基石。下面再回到生成 + Promise的异步的解析
在生成器中并发执行Promise
在上面的写法中是没办法并发执行的, 想要实现并发
function *foo() {
const p1 = request("https://some.url.1");
const p2 = request("https://some.url.2");
const r1 = yield p1;
const r2 = yield p2;
const r3 = yield request("https://some.url.3/?v=" + r1 + "," + r2);
console.log(r3);
}
run(foo);
这里实现并发的办法就是让异步请求先出发,等所有请求都执行后我们再yield Promise,也可以使用Promise.all实现并发, 下面覆写这个例子
function *foo() {
const result = yield Promise.all(
request("https://some.url.1"),
request("https://some.url.2"));
const r3 = yield request("https://some.url.3/?v=" + r1 + "," + r2);
console.log(r3);
}
我们还可以把Promise.all封装在一个函数中,使得foo生成器的简洁性,这样从生成器的角度看并不需要关系底层的异步是怎么实现的,我们实现生成器 + Promise要尽量把异步逻辑封装在底层
生成器委托
怎么在一个生成器中调用另一个生成器,并且重要的是,对待调用的生成器内的异步代码就像直接写在生成器内部一样(也就是说也能顺序执行调用的生成器内部的代码不管同步异步)
function *foo() {
const r2 = yield request("https://some.url.2");
const r3 = yield request("htpps://some.url.3/?v=" + r2);
return r3;
}
function *bar() {
const r1 = yield request("http://some.url.1");
//通过run 函数调用foo
const r3 = yield run(foo);
console.log(r3);
}
run(bars);
为什么可以这样,因为我们run函数是返回一个Promise的,就像上面说的,我们把Promise的细节封装了,通过run(foo)你只需要直到两件事,run(foo)返回一个Promise,既然是Promise,我们就yield就可以了, 同时run(foo)产生的Promise完成的时候就是foo完成的时候,决议值就是r3,如果对前面说的感到不理解,我再简单的补充一点,就是run本意就是返回一个Promise,,既然是Promise,我们当前可以像yield request那样yield它而不用管底层细节, es有一种称为生成器委托的语法 yield*,先看下面一个简单的用法介绍yield *,
function *foo () {
console.log("*foo() starting");
yield 3;
yield 4;
console.log("*foo() finished");
}
function *bar() {
yield 1;
yield 2;
yield *foo();
yield 5;
}
const it = bar();
it.next().value // 1
it.next().value //2
it.next().value
// *foo() starting
// 3
it.next().value
//4
it.next().value
//*foo() finished
// 5
当我们消费完bar的前两个yield后,再next,这个时候,控制权转给了foo,这个时候控制的是foo而不是bar,这也就是为什么称之为委托,因为bar把自己的迭代控制委托给了foo,要是在控制foo的时候一直不next,bar也没办法进行了,当it迭代器控制消耗完了整个foo后,控制权就会自动转回bar,我们现在可以使用生成器委托覆写上述生成器委托下的第一个例子,在那个例子中使用了run(foo);我们现在可以让生成器更 `干净一点`
function *foo() {
const r2 = request("https://some.url.2");
const r3 = request("https://some.url.3?v=" + r2);
return r3;
}
function *bar() {
const r1 = request("https://some.url.1");
const r3 = yield *foo();
console.log(r3);
}
run(bar);
生成器委托其实就相当于函数调用,用来组织分散的代码,
消息委托
生成器委托的作用不只在于控制生成器,也可以用它实现双向消息传递工作,请看下面一个例子
function *foo() {
console.log("inside *foo(): ", yield "B");
console.log("inside *foo():", yield "C");
return "D";
}
function *bar() {
console.log("inside *bar():", yield "A");
console.log("inside *bar(): ", yield *foo());
console.log("inside *bar(): ", yield "E");
return "F";
}
const it = bar();
console.log("outSide:", it.next().value);
//outside: "A"
console.log("outside", it.next(1).value);
//inside *bar: 1
//outside: B;
console.log("outside", it.next(2).value);
//inside *foo: 2
// outside: C
console.log("outside:", it.next(3).value);
//inside *foo 3
//inside *bar: D
//outside: "E"
console.log("outside:", it.next(4).value);
//inside *bar: 4
//outside: E
在这里我们就实现了和委托的生成器传递消息,外界传入2,3都传递到了foo中,foo yield的B,C页传递给了外界迭代器控制方(it),除此之外错误和异常也可以被双向传递,
function *foo() {
try {
yield "B";
} catch (err) {
console.log("error caught inside *foo():", err);
}
yield "C";
throw "D";
}
functtion *baz() {
throw "F";
}
function *bar() {
yield "A";
try {
yield *foo();
} catch (err) {
console.log("error caugth inside *bar(): ", err);
}
yield "E";
yield *baz();
yield "G";
}
const it = bar();
console.log("outside:", it.next().value);
//outside: A;
console.log("outside:", it.next(1).value);
//outside: B
console.log("outside:", it.throw(2).value);//外界向内抛入一个错误
//error caugth inside *foo () 2
//"outside: C"
console.loog("ouside:",it.next(3).value);
//error caugth insde *bar "D";
// ouside: E
try {
console.log("outside", it.next(4).value);
} catch(err) {
console.log("error cautgh outside:", err);
}
//error caugth ouside: F
//控制器结束
通过以上,我们可以总结出,当时有生成器委托的时候,和正常生成器其实没有什么区别对于外界的控制器(it)来说,它不在乎控制的是foo还是bar抑或是baz,它把这些看作是一个生成器,就像和一个生成器那样和其内部的各生成器进行双向的信息传递
生成器并发
在上面我们讨论过生成器并发Promise,在这里我们讨论并发生成器,
const res = [];
function *reqData(url) {
res.push(yield request(url));
}
const it1 = reqData("https://some.url.1");
const it2 = reqData("https://some.url.2");
const p1 = it1.next();
const p2 = it2.next();
p1.
then(function (data) {
it1.next(data);
return p2;
}).then(function (data) {
it2.next(data);
})
这里的生成器是并发的,并且通过then给这两个生成器安排好了结果位置,但是,这段代码手工程度很高,没办法让生成器自动的协调,
看下面一个例子
function runAll(...args) {
const result = [];
//同步并发执行Promise
args = args.forEach(function (item) {
item = item();
item.next();
return item;
});
function * fn() {
args.forEach(function (item,idx) {
let p = item.next();
res[idx] = yield p;
});
};
run(fn);
return result;
}
runAll(function *() {
const p1 = request("....");
yield;
res.push(yield p1);
}, function *() {
const p2 = request(".....");
yield;
res.push(yield p2);
});
这个例子避免了手动的去书写Promise的then链,但是这样的写法也不算是真正实现生成器并发,真正的runAll很复杂,所以没有提出
总结
生成器异步就是生成器加Promise,要求yield出一个Promise,由外部控制,但在现在完全可以使用async/await