使用Generator函数进行异步编程

Generator函数在工作中还没有用到过,一直在使用async,最近在看async的原理,发现它只是Generator的语法糖。

Generator的基础知识之前写过文章介绍过(https://www.cnblogs.com/wangtingnoblog/p/js_Generator.html),这里主要讨论一下怎么使用Generator函数来进行异步编程。

先来看下面的代码:

1 function* g() {
2   console.log('g start')
3   yield setTimeout(() => console.log('setTimeout'), 3000)
4   console.log('g end')
5 }
6 const useg = g();
7 useg.next();
8 useg.next();

运行结果:

分析:

  • 第7步时,yield后面的异步操作已经开始,但是还没有结束,会在3秒后打印出setTimeout,
  • 因为没有等到异步操作结束就调用了next方法,造成先打印出 g end,然后才打印出setTimeout
  • 我们想要的是按照正常的顺序进行打印,比如在得知yield完成之后才能结束函数,这个要怎么实现呢?
  • 如果在yield表达式执行完成后能执行一个回调函数,我们在回调函数中再通知Generator函数执行下一步就可以了。

看下面的代码:

 1 function* g() {
 2   console.log('g start')
 3   yield function(callBack) {
 4     setTimeout(() => {
 5       console.log('setTimeout')
 6       callBack()
 7     }, 3000)
 8   }
 9   console.log('g end')
10 }
11 const useg = g();
12 const first = useg.next();
13 first.value(() => useg.next());

 运行结果:

分析:

  • 可以看到,我们把异步操作封装在一个函数中,这个函数只接受一个回调函数作为参数,当异步操作执行完成会,它会调用回调函数。
  • 这样就保证Generator函数的下部分的代码一定在上部分的代码执行完成之后才执行。
  • 有一点需要注意的是,这样修改代码使得异步操作变成了惰性执行了。
  • 之前的代码是只要调用useg.next方法,异步操作就会开始执行,修改后的代码则变成就算调用了useg.next方法,如果没有调用value(callback),异步操作也不会执行。
  • 这种惰性执行和Observable很像,如果没有订阅Observable,Observable的逻辑就不会执行
  • Promise是立即执行的

我们再来看一下实际的例子,使用Generator函数封装ajax:

 1 function* g() {
 2   console.log('g start')
 3   const userName = yield function(callBack) {
 4     const xhr = new XMLHttpRequest();
 5     xhr.open('get', 'http://localhost:3002/users?id=1', true);
 6     xhr.responseType = 'json';
 7     xhr.setRequestHeader('Accept', 'application/json');
 8     xhr.onload = () => {
 9       const data = xhr.response;
10       callBack(data[0].name);
11     }
12     xhr.send(null);
13   }
14   const families = yield function(callBack) {
15     const xhr = new XMLHttpRequest();
16     xhr.open('get', `http://localhost:3002/family?name=${userName}`, true);
17     xhr.responseType = 'json';
18     xhr.setRequestHeader('Accept', 'application/json');
19     xhr.onload = () => {
20       const data = xhr.response;
21       callBack(data[0].families);
22     }
23     xhr.send(null);
24   }
25   console.log(families)
26   console.log('g end')
27 }
28 const useg = g();
29 const first = useg.next();
30 first.value(
31   (name) => {
32     const second = useg.next(name)
33     second.value(
34       (families) => useg.next(families)
35     )
36   }
37 );

上面的代码执行的是:先通过id取得用户名,再通过用户名取得家庭成员。只看第1行到第27行,步骤清晰,很像是同步的写法。 

我们需要关注的是,第28行到第37行对Generator函数的流程管理:

  1. 调用Generator函数取得迭代器对象
  2. 调用next方法取得value属性
  3. 调用value属性的值,给它传入回调函数
  4. 在回调函数中执行第2步,第3步
  5. 判断此时函数已经执行完成

注意到这里的12345步是可以用迭代来进行的,修改流程管理的代码如下:

 1 function run(g) {
 2   // 取得迭代器
 3   const useg = g();
 4   // 定义循环调用的函数
 5   function loop(data) {
 6     // 取得当前的值
 7     const result = useg.next(data);
 8     // 判断是否已经执行完成,执行完成会退出
 9     if (result.done) return;
10     // 没有执行完成则继续执行回调
11     result.value(loop);
12   }
13   // 开始迭代
14   loop();
15 }
16 run(g);

 可以看到,使用迭代来管理流程时完全没有必要知道生成器的内部结构。而且这种方式也不管生成器中有多少个yield。

有一点需要注意的是,使用这种发生来管理流程要求生成器中yield后面的表达式必须是一个函数,且这个函数有唯一参数(回调函数)

其实,我们需要的只是在异步操作有了结果之后把执行权再交还给Generator函数继续执行。

问题是怎么知道异步操作有了结果?

  1. 回调函数
  2. Promise

上面已经介绍过使用回调函数来控制流程,它的限制是yield后面的表达式必须是一个以回调函数为参数的函数。

 下面介绍使用Promise来控制流程:

ajax函数返回Promise 

 1 function ajax(url) {
 2   return new Promise(
 3     (resolve, reject) => {
 4       const xhr = new XMLHttpRequest();
 5       xhr.open('get', url, true);
 6       xhr.responseType = 'json';
 7       xhr.setRequestHeader('Accept', 'application/json');
 8       xhr.onload = () => {
 9         const data = xhr.response;
10         resolve(data);
11       }
12       xhr.send(null);
13     }
14   )
15 }
View Code
 1 function* g() {
 2   console.log('g start')
 3   const userNameResponse = yield ajax('http://localhost:3002/users?id=1')
 4   const familiesResponse = yield ajax('http://localhost:3002/family?name=' + userNameResponse[0].name)
 5   console.log(familiesResponse[0].families)
 6   console.log('g end')
 7 }
 8 const useg = g();
 9 useg.next().value.then(
10   (userNameResponse) => useg.next(userNameResponse).value.then(
11     (familiesResponse) => useg.next(familiesResponse)
12   )
13 );

可以看到:

  1. yield后面必须是Promise对象
  2. 流程控制中同样可以使用迭代来执行

Promise迭代版本的流程控制:

function run(g) {
  // 取得迭代器
  const useg = g();
  // 定义循环调用的函数
  function loop(data) {
    // 当前的值
    const result = useg.next(data);
    // 判断是否已经执行完成,执行完成会退出
    if (result.done) return;
    // 没有执行完成则继续执行回调
    result.value.then(loop);
  }
  // 开始迭代
  loop();
}
run(g);

对比之前的使用回调函数来控制流程的代码,你会发现和使用Promise来控制流程的代码如此的类似。

使用Promise来控制流程需要注意的是yield后面必须是一个Promise对象

对比使用回调函数控制流程和使用Promise对象来控制流程

  • 使用回调函数控制流程需要yield后面是一个以回调函数为参数的函数,使用Promise对象来控制流程需要yield后面必须是Promise对象。
  • 使用回调函数控制流程时,异步操作是惰性执行的,使用Promise对象来控制流程时,异步操作是立即执行的

总结:

Generator函数本身使得异步操作看起来非常像同步操作,麻烦的是它的流程控制需要我们手动调用。(之后要讨论的async就是对Generator的进一步封装,不用开发者总结来流程控制)

另外上面的例子为了简化,都没有做异常处理,实际开发中,异常处理还是很有必要的。

作成:2019-02-10

修改:

  1. 2019-02-10 23:23:57 添加分类

参考:《ES6标准入门》、《Learning TypeScript》、《深入理解ES6》

posted on 2019-02-10 20:04  西门本不吹雪  阅读(581)  评论(0编辑  收藏  举报