生成器下(生成器异步)

0 阅读7分钟

生成器下(生成器异步)

上一章讨论了生成器作为一个产生值的机制的特性,但生成器远远不止这些,更多的时候我们关注的是生成器在异步编程中的使用

生成器 + 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