ES6 generator 如何实现异步编程
2018/04/12
先从同步操作说起。
首先我们有一个函数,可以返回把传入参数加一的结果。
var syncPlusOne = function(a) {
return a+1
}
如果我们需要把每一次调用的结果作为下一次调用的参数,就这样调用:
var continuousSyncPlusOne = function (s) {
var a = syncPlusOne(s)
var b = syncPlusOne(a)
var c = syncPlusOne(b)
}
如果我们现在有一个异步Plus函数呢?
var wrongAsyncPlusOne = function(a) {
setTimeout(function() {
return a+1
}, Math.random()*1000)
}
调用这个函数,会直接返回undefined
,因为异步过程会直接返回,而不会阻塞。对于一个异步过程,我们有两种方法可以得知它的计算过程是否结束。
一种办法是轮训,比如如下代码:
var tv
setTimeout(function (){
tv = 1
}, 1000);
while (true) {
if (tv != undefined) {
console.log("tv is set to :" + tv);
break;
}
}
我们在while循环中不断的轮询tv的值,直到tv被赋值。但很不幸,这个代码并不会按照我们的想法执行,这与javascript的运行机制有关,这里不展开讲,正确的写法如下:
var tv
setTimeout(function (){
tv = 1
}, 1000);
var intervalId = setInterval(function() {
if (tv != undefined) {
console.log("tv is set to :" + tv);
clearInterval(intervalId);
}
}, 0)
另一种方法,是这个函数执行结束后自己告诉我们:
var tv
setTimeout(function (){
tv = 1
console.log("tv is set to :" + tv)
}, 1000);
我们在函数体内调用 console.log()
来表示 tv 的值已经被计算好了,本质上是将 tv 计算好之后的值,主动传递给了后续过程。如果把需要这个异步计算结果的函数作为参数传递进来,并在计算结束后将结果传递给它,那么这个函数就被称为回调函数。
(setTimeout
的第一个参数传入的也是一个回调函数,当计时结束后,会调用回调函数。其实 setTimeout
和 setInterval
的第三个可选参数可以给回调函数传参。)
那么重新改写我们之前的异步Plus如下:
var asyncPlusOne = function(a, callback) {
setTimeout(function() {
callback(a+1)
}, Math.random()*1000)
}
如果我们需要得到某个数字连续调用asyncPlusOne之后的结果,则要写为:
var continuousAsyncPlusOne = function(s) {
asyncPlusOne(s, function(a) {
asyncPlusOne(a, function(b) {
asyncPlusOne(b, function(c) {
console.log(c); // 3
})
})
})
}
continuousAsyncPlusOne(0);
es6语法可以这样写:
var continuousAsyncPlusOne = function(s) {
asyncPlusOne(s, (a) => {
asyncPlusOne(a, (b) => {
asyncPlusOne(b, (c) => {
console.log(c); // 3
})
})
})
}
第一次写这种代码,感觉还挺酷的样子。但是久而久之,代码中到处都是这种callback结构,太不简洁了,这被称为 callback hell。
那么我们如何把异步调用变成同步的书写方式呢?比如像这样子:
var a = asyncPlusIWant(s)
var b = asyncPlusIWant(a)
var c = asyncPlusIWant(b)
console.log(c);
我们希望:
- asyncPlusIWant() 是异步的
- 它能立即返回计算之后的结果(而不是undifined)
这两条不是自相矛盾吗。唉,如果我们的语句走到 asyncPlusIWant() 时,能够停下来,等待这个异步过程执行完成之后,再继续执行下面的语句,就好了。
Wait! 停下来、再继续,好熟悉的东西,ES6 引入的 Generator 不是可以做到这件事吗!
把我们的语句放到 Generator 中看看!
var genAsync = function* (s) {
var a = yield asyncPlusOne(s)
console.log(a) // undefined
var b = yield asyncPlusOne(a)
var c = yield asyncPlusOne(b)
}
console.log(g.next().value); // undefined
并没有得到正确的输出,不是说好的会停下来的吗?
其实这里确实停了,只不过不是我们想象的那种停,这里两个地方输出都是undefined
原因如下:
console.log(a)
输出undefined
是因为 yield <expression> 的值取决于外部调用next时传入的值,外部调用g.next()时候并没有传值进去。console.log(g.next().value)
输出undefined
是因为g.next().value
应该拿到的是表达式asyncPlusOne(s)
的值,而asyncPlusOne
是异步的,它直接返回了undefined
。
那么如何才可以结合 Generator 函数实现异步过程的同步调用呢?
为了实现我们的目标,考虑如下辅助函数:
var g = function(f){
return function (args){
return function (callback){
return f(args, callback)
}
}
}
那么
\[\mathrm{f(s, callback) = g(f)(s)(callback)}\]好吧,你看出来了,这里就是做了一个柯里化。
那我们令
\[\mathrm{asyncPlusIWant = g(asyncPlusOne)}\]var asyncPlus = function (a, callback) {
setTimeout(function () {
callback(a + 1);
}, Math.random() * 1000);
}
var asyncPlusIWant = g(asyncPlus)
var genAsync = function* (s) {
var a = yield asyncPlusIWant(s);
console.log('async a: ' + a)
var b = yield asyncPlusIWant(a);
console.log('async b: ' + b)
var c = yield asyncPlusIWant(b);
console.log('async c: ' + c)
return c+1
};
var run = function run(gen) {
function go(lastRes) {
// 1.[唤醒],把上一次计算出的值放到LHS(left-hand side),然后移动到下一个异步调用的位置停下来
var res = gen.next(lastRes);
if (res.done) return;
// 2.[执行],调用res.value(go)将开始执行异步计算,计算完成后调用会调用go继续唤醒generator
res.value(go);
}
go()
}
run(genAsync(0));
这里的辅助函数 g 被称作 Thunk 函数。
var Thunk = function(fn){
return function (){
var args = Array.prototype.slice.call(arguments);
return function (callback){
args.push(callback);
return fn.apply(this, args);
}
};
};
var thunkFunc = Thunk(func); 可以把fn包裹成另一个函数 thunkFunc
他改变了原来 func 的调用方式:
func(arg_1, arg_2, ... , arg_k, callback)
func(arg_1, arg_2, ... , arg_k)(callback)
通过上面的 run() 方法,执行 run(genAsync) 就可以依次执行三个异步函数了。
这件很酷的事情本质是什么?
使用 Thunk 和 Generator 使异步函数以同步的方式书写,本质是:
Thunk 把异步函数的「执行」从 Generator 内,分离到了 Generator 外部,把 Generator 的「唤醒」放到了「在外部执行的异步操作的回调函数内」,所以整个执行流程变成了,在外部执行的异步函数,通过回调,不断的去唤醒 Generator 继续执行后续操作。