背景
由于最近一段时间一直在用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
果然!如之前预料的一样的执行效果,那么把这段代码补全,即可以达到顺序输出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
现在已经通过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
,从而对同步、异步代码进行流程控制的思考与总结。