Javascript 中通过 yield 和 promise 使异步变同步

背景

由于最近一段时间一直在用react-native进行APP的开发,所以接触了不少 javascript

在我们第一次使用react-native + redux + saga开发的过程中,学习、见识到了不少javascript神奇的功能,比如在使用saga的过程中用到了 yield,并且对于其使得异步操作同步化十分好奇,就进行了一番探索。

yield简单介绍

先看一段简单的代码

function* gen() {
  yield console.log(1)
  yield console.log(2)
  console.log(3)
}

const g = gen()
g.next()
g.next()
g.next()

输出如下

1
2
3

函数gen的声明使用了function*,使得gen函数成为一个generator,并且可以在其中里面使用yield关键字,gen()返回一个generator对象,通过next()依次调用。

在我的理解看来,可以将yield关键字理解为函数的断点,每次next()就会从上次的断点(yield)执行到下次的断点(yield),直到函数结束退出,于是就产生了上面的结果。

next() 的返回值

修改一下上面程序的输出代码,用来查看一下next()函数的返回值

const g = gen()
console.log(g.next())
console.log(g.next())
console.log(g.next())

输出:

1
{ value: undefined, done: false }
2
{ value: undefined, done: false }
3
{ value: undefined, done: true }

可以看到,next()函数的返回值是一个{value: any, done: boolean}objectvalue是运行到这个断点的返回值,done表示该函数是否已经执行完毕,对next()方法返回值的了解,会有助于我们下面的程序实现。

promise 的简单了解

promise的出现,是为了解决javascript中的异步无限回调,一般的使用方法如下:

function delay(ms) {  
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms);
  });
}

console.log(1)
delay(300).then(() => console.log(2))
console.log(3)

输出:

1
3
2

delay()函数中使用setTimeout()来模拟一个异步操作,再用promise封装一下,使得可以使用promisethen()方法来在异步操作完成之后,执行特定的代码。

现在promise的使用已经很普遍,javascript标准中的fetch()函数也是支持promise的回调机制,以方便开发者更容易的处理网络请求的异步返回。

异步操作 – 问题的产生

在我们的使用过程中,有类似如下结构的代码:

function* baum() {
  yield delay(300).then(() => console.log(1))
  yield console.log(2)
  yield delay(300).then(() => console.log(3))
  yield console.log(4)
}

const b = baum()
b.next()
b.next()
b.next()
b.next()

输出如下:

2
4
1
3

函数baum()结构表达的意思是,有一些同步的操作,然后会发出异步的请求(比如网络请求),异步请求结束后,再执行后面的代码。但是因为delay()函数的异步使得13的输出延迟了,并没有达到预期效果。

可是令人十分费解的是,在saga中,这样的程序结构,是会按照顺序执行的效果呈现出来,即输出是1,2,3,4,所以一定是saga在对诸如baum()这样的generator进行了一层包裹,使得里面的同步操作可以等待上一个异步promise函数执行完成后再被触发。

异步变同步 – 机械的实现

为了可以使得代码的执行顺序变成函数里面的书写顺序,我们先看一下baum()函数每次next()的返回值都是什么,对上一节的输出代码稍作改写:

const b = baum()
console.log(b.next())

输出:

{ value: Promise { <pending> }, done: false }
1

上面第一行输出中的value值,是delay(300).then(() => console.log(1))这段代码的执行结果,那么我们可知,对于一个promise,它的then()函数的返回值同样是一个promise对象。由此来说,只要将同步的next()执行,放到它前面异步的promise中的then()函数里,即可以达到同步代码发生在异步代码操作之后的效果了。

说起来有点绕,看一下改进之后的代码:

const b = baum()
b.next().value.then(() => {
  b.next()
})

输出:

1
2

果然!如之前预料的一样的执行效果,那么把这段代码补全,即可以达到顺序输出1,2,3,4的效果了:

const b = baum()
b.next().value.then(() => { // 第一个 delay 函数返回的 promise
  b.next()
  b.next().value.then(() => { // 第二个 delay 函数返回的 promise
    b.next()
  })
})

输出:

1
2
3
4

现在已经通过promisethen()方法,做到了异步、同步代码执行时的所见即所得,即程序的输出顺序,是和书写顺序一致的。

那么最后的任务,就是对上面的代码进行封装,以免去这种手工机械化的调用。

异步变同步 – 自动化的实现

经过一番折腾,最后写出了下面一个函数himmel()来使得generator中的调用,无论异步的还是同步的yield操作,都是依照着代码的书写顺序执行的:

function himmel(gen) {
  const item = gen.next()
  if (item.done) {
    return item.value
  }

  const { value, done } = item
  if (value instanceof Promise) {
    value.then((e) => himmel(gen))
  } else {
    himmel(gen)
  }
}

函数的实现是一个递归,接收参数是一个generator实例,退出条件即为当yield结果中的donetrue的时候。后面的代码会判断value是否是一个promise,如果是的话,就在then()方法中递归,否则就认为是同步代码,直接递归。

测试一下:

himmel(baum())

输出:

1
2
3
4

结果也是正确的。

验证以上思路的可行性

yield出现的时候,就随之出现了一个比较有名的库叫作 co ,这个库的作用就是控制同步与异步代码的执行顺序,在它的说明中原文是 generator based flow-control

看过co的实现代码之后,发现其本质上也是这么实现的。只不过那个库加上了更多的边界检测代码,做的更加健壮。

至此,就是我对于在javascript中使用yield+promise,从而对同步、异步代码进行流程控制的思考与总结。

    原文作者:zijing07
    原文地址: https://www.jianshu.com/p/c1b8b89c4905
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞