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}的object,value是运行到这个断点的返回值,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封装一下,使得可以使用promise的then()方法来在异步操作完成之后,执行特定的代码。
现在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()函数的异步使得1和3的输出延迟了,并没有达到预期效果。
可是令人十分费解的是,在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
3
4
现在已经通过promise的then()方法,做到了异步、同步代码执行时的所见即所得,即程序的输出顺序,是和书写顺序一致的。
那么最后的任务,就是对上面的代码进行封装,以免去这种手工机械化的调用。
异步变同步 - 自动化的实现
经过一番折腾,最后写出了下面一个函数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结果中的done为true的时候。后面的代码会判断value是否是一个promise,如果是的话,就在then()方法中递归,否则就认为是同步代码,直接递归。
测试一下:
himmel(baum())
输出:
1
2
3
4
结果也是正确的。
验证以上思路的可行性
在yield出现的时候,就随之出现了一个比较有名的库叫作 co ,这个库的作用就是控制同步与异步代码的执行顺序,在它的说明中原文是 generator based flow-control。
看过co的实现代码之后,发现其本质上也是这么实现的。只不过那个库加上了更多的边界检测代码,做的更加健壮。
至此,就是我对于在javascript中使用yield+promise,从而对同步、异步代码进行流程控制的思考与总结。
原文链接:https://www.jianshu.com/p/c1b8b89c4905