Thunks
至此,我们都假定从一个generator中yield
一个Promise——让这个Promise使用像run(..)
这样的帮助工具来推进generator——是管理使用generator的异步处理的最佳方法。明白地说,它是的。
但是我们跳过了一个被轻度广泛使用的模式,为了完整性我们将简单地看一看它。
在一般的计算机科学中,有一种老旧的前JS时代的概念,称为“thunk”。我们不在这里赘述它的历史,一个狭隘的表达是,thunk是一个JS函数——没有任何参数——它连接并调用另一个函数。
换句话讲,你用一个函数定义包装函数调用——带着它需要的所有参数——来 推迟 这个调用的执行,而这个包装用的函数就是thunk。当你稍后执行thunk时,你最终会调用那个原始的函数。
举个例子:
function foo(x,y) {
return x + y;
}
function fooThunk() {
return foo( 3, 4 );
}
// 稍后
console.log( fooThunk() ); // 7
所以,一个同步的thunk是十分直白的。但是一个异步的thunk呢?我们实质上可以扩展这个狭隘的thunk定义,让它接收一个回调。
考虑这段代码:
function foo(x,y,cb) {
setTimeout( function(){
cb( x + y );
}, 1000 );
}
function fooThunk(cb) {
foo( 3, 4, cb );
}
// 稍后
fooThunk( function(sum){
console.log( sum ); // 7
} );
如你所见,fooThunk(..)
仅需要一个cb(..)
参数,因为它已经预先制定了值3
和4
(分别为x
和y
)并准备传递给foo(..)
。一个thunk只是在外面耐心地等待着它开始工作所需的最后一部分信息:回调。
但是你不会想要手动制造thunk。那么,让我们发明一个工具来为我们进行这种包装。
考虑这段代码:
function thunkify(fn) {
var args = [].slice.call( arguments, 1 );
return function(cb) {
args.push( cb );
return fn.apply( null, args );
};
}
var fooThunk = thunkify( foo, 3, 4 );
// 稍后
fooThunk( function(sum) {
console.log( sum ); // 7
} );
提示: 这里我们假定原始的(foo(..)
)函数签名希望它的回调的位置在最后,而其它的参数在这之前。这是一个异步JS函数的相当普遍的“标准”。你可以称它为“回调后置风格”。如果因为某些原因你需要处理“回调优先风格”的签名,你只需要制造一个使用args.unshift(..)
而非args.push(..)
的工具。
前面的thunkify(..)
公式接收foo(..)
函数的引用,和任何它所需的参数,并返回thunk本身(fooThunk(..)
)。然而,这并不是你将在JS中发现的thunk的典型表达方式。
与thunkify(..)
制造thunk本身相反,典型的——可能有点儿让人困惑的——thunkify(..)
工具将产生一个制造thunk的函数。
额…是的。
考虑这段代码:
function thunkify(fn) {
return function() {
var args = [].slice.call( arguments );
return function(cb) {
args.push( cb );
return fn.apply( null, args );
};
};
}
这里主要的不同之处是有一个额外的return function() { .. }
。这是它在用法上的不同:
var whatIsThis = thunkify( foo );
var fooThunk = whatIsThis( 3, 4 );
// 稍后
fooThunk( function(sum) {
console.log( sum ); // 7
} );
明显地,这段代码隐含的最大的问题是,whatIsThis
叫什么合适?它不是thunk,它是一个从foo(..)
调用生产thunk的东西。它是一种“thunk”的“工厂”。而且看起来没有任何标准的意见来命名这种东西。
所以,我的提议是“thunkory”(”thunk” + “factory”)。于是,thunkify(..)
制造了一个thunkory,而一个thunkory制造thunks。这个道理与第三章中我的“promisory”提议是对称的:
var fooThunkory = thunkify( foo );
var fooThunk1 = fooThunkory( 3, 4 );
var fooThunk2 = fooThunkory( 5, 6 );
// 稍后
fooThunk1( function(sum) {
console.log( sum ); // 7
} );
fooThunk2( function(sum) {
console.log( sum ); // 11
} );
注意: 这个例子中的foo(..)
期望的回调不是“错误优先风格”。当然,“错误优先风格”更常见。如果foo(..)
有某种合理的错误发生机制,我们可以改变而使它期望并使用一个错误优先的回调。后续的thunkify(..)
不会关心回调被预想成什么样。用法的唯一区别是fooThunk1(function(err,sum){..
。
暴露出thunkory方法——而不是像早先的thunkify(..)
那样将中间步骤隐藏起来——可能看起来像是没必要的混乱。但是一般来讲,在你的程序一开始就制造一些thunkory来包装既存API的方法是十分有用的,然后你就可以在你需要thunk的时候传递并调用这些thunkory。这两个区别开的步骤保证了功能上更干净的分离。
来展示一下的话:
// 更干净:
var fooThunkory = thunkify( foo );
var fooThunk1 = fooThunkory( 3, 4 );
var fooThunk2 = fooThunkory( 5, 6 );
// 而这个不干净:
var fooThunk1 = thunkify( foo, 3, 4 );
var fooThunk2 = thunkify( foo, 5, 6 );
不管你是否愿意明确对付thunkory,thunk(fooThunk1(..)
和fooThunk2(..)
)的用法还是一样的。
s/promise/thunk/
那么所有这些thunk的东西与generator有什么关系?
一般性地比较一下thunk和promise:它们是不能直接互换的,因为它们在行为上不是等价的。比起单纯的thunk,Promise可用性更广泛,而且更可靠。
但从另一种意义上讲,它们都可以被看作是对一个值的请求,这个请求可能被异步地应答。
回忆第三章,我们定义了一个工具来promise化一个函数,我们称之为Promise.wrap(..)
——我们本来也可以叫它promisify(..)
的!这个Promise化包装工具不会生产Promise;它生产那些继而生产Promise的promisories。这和我们当前讨论的thunkory和thunk是完全对称的。
为了描绘这种对称性,让我们首先将foo(..)
的例子改为假定一个“错误优先风格”回调的形式:
function foo(x,y,cb) {
setTimeout( function(){
// 假定 `cb(..)` 是“错误优先风格”
cb( null, x + y );
}, 1000 );
}
现在,我们将比较thunkify(..)
和promisify(..)
(也就是第三章的Promise.wrap(..)
):
// 对称的:构建问题的回答者
var fooThunkory = thunkify( foo );
var fooPromisory = promisify( foo );
// 对称的:提出问题
var fooThunk = fooThunkory( 3, 4 );
var fooPromise = fooPromisory( 3, 4 );
// 取得 thunk 的回答
fooThunk( function(err,sum){
if (err) {
console.error( err );
}
else {
console.log( sum ); // 7
}
} );
// 取得 promise 的回答
fooPromise
.then(
function(sum){
console.log( sum ); // 7
},
function(err){
console.error( err );
}
);
thunkory和promisory实质上都是在问一个问题(一个值),thunk的fooThunk
和promise的fooPromise
分别代表这个问题的未来的答案。这样看来,对称性就清楚了。
带着这个视角,我们可以看到为了异步而yield
Promise的generator,也可以为异步而yield
thunk。我们需要的只是一个更聪明的run(..)
工具(就像以前一样),它不仅可以寻找并连接一个被yield
的Promise,而且可以给一个被yield
的thunk提供回调。
考虑这段代码:
function *foo() {
var val = yield request( "http://some.url.1" );
console.log( val );
}
run( foo );
在这个例子中,request(..)
既可以是一个返回一个promise的promisory,也可以是一个返回一个thunk的thunkory。从generator的内部代码逻辑的角度看,我们不关心这个实现细节,这就它强大的地方!
所以,request(..)
可以使以下任何一种形式:
// promisory `request(..)` (见第三章)
var request = Promise.wrap( ajax );
// vs.
// thunkory `request(..)`
var request = thunkify( ajax );
最后,作为一个让我们早先的run(..)
工具支持thunk的补丁,我们可能会需要这样的逻辑:
// ..
// 我们收到了一个回调吗?
else if (typeof next.value == "function") {
return new Promise( function(resolve,reject){
// 使用一个错误优先回调调用thunk
next.value( function(err,msg) {
if (err) {
reject( err );
}
else {
resolve( msg );
}
} );
} )
.then(
handleNext,
function handleErr(err) {
return Promise.resolve(
it.throw( err )
)
.then( handleResult );
}
);
}
现在,我们generator既可以调用promisory来yield
Promise,也可以调用thunkory来yield
thunk,而不论那种情况,run(..)
都将处理这个值并等待它的完成,以继续generator。
在对称性上,这两个方式是看起来相同的。然而,我们应当指出这仅仅从Promise或thunk表示延续generator的未来值的角度讲是成立的。
从更高的角度讲,与Promise被设计成的那样不同,thunk没有提供,它们本身也几乎没有任何可靠性和可组合性的保证。在这种特定的generator异步模式下使用一个thunk作为Promise的替代品是可以工作的,但与Promise提供的所有好处相比,这应当被看做是一种次理想的方法。
如果你有选择,那就偏向yield pr
而非yield th
。但是使run(..)
工具可以处理两种类型的值本身没有什么问题。
注意: 在我们将要在附录A中讨论的,我的 asynquence 库中的runner(..)
工具,可以处理yield
的Promise,thunk和 asynquence 序列。