4.7 形实转换程序
目前为止,我们已经假定从生成器 yield 出一个 Promise,并且让这个 Promise 通过一个像 run(..) 这样的辅助函数恢复这个生成器,这是通过生成器管理异步的最好方法。要知道,事实的确如此。
但是,我们忽略了另一种广泛使用的模式。为了完整性,我们来简要介绍一下这种模式。
在通用计算机科学领域,有一个早期的前 JavaScript 概念,称为形实转换程序 (thunk)。我们这里将不再陷入历史考据的泥沼,而是直接给出形实转换程序的一个狭义表述:JavaScript 中的 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(..) )函数原型需要的回调放在最后的位置,其他参数都在它之前。对异步 JavaScript 函数标准来说,这可以说是一个普遍成立的标准。你可以称之为“callback-last 风格”。如果出于某种原因需要处理“callback-first 风格”原型,你可以构建一个使用 args.unshift(..) 而不是 args.push(..) 的工具。
前面 thunkify(..) 的实现接收 foo(..) 函数引用以及它需要的任意参数,并返回 thunk 本身(fooThunk(..) )。但是,这并不是 JavaScript 中使用 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 生成 thunk。这和第 3 章中我提议 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(..) 例子要求回调的风格不是 error-first 风格。当然,error-first 风格要常见得多。如果 foo(..) 需要满足一些正统的错误生成期望,可以把它按照期望改造,使用一个 error-first 回调。后面的 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 的内容与生成器有什么关系呢?
可以把 thunk 和 promise 大体上对比一下:它们的特性并不相同,所以并不能直接互换。Promise 要比裸 thunk 功能更强、更值得信任。
但从另外一个角度来说,它们都可以被看作是对一个值的请求,回答可能是异步的。
回忆一下,在第 3 章里我们定义了一个工具用于 promise 化一个函数,我们称之为 Promise.wrap(..) ,也可以将其称为 promisify(..) !这个 Promise 封装工具并不产生 Promise,它生成的是 promisory,而 promisory 则接着产生 Promise。这和现在讨论的 thunkory 和 thunk 是完全对称的。
为了说明这种对称性,我们要首先把前面的 foo(..) 例子修改一下,改成使用 error-first 风格的回调:
function foo(x,y,cb) { setTimeout( function(){ // 假定cb(..)是error-first风格的 cb( null, x + y ); }, 1000 ); }
现在我们对比一下 thunkify(..) 和 promisify(..) (即第 3 章中的 Promise.wrap(..) )的使用:
// 对称:构造问题提问者 var fooThunkory = thunkify( foo ); var fooPromisory = promisify( foo ); // 对称:提问 var fooThunk = fooThunkory( 3, 4 ); var fooPromise = fooPromisory( 3, 4 ); // 得到答案 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 以获得异步性的生成器,也可以为异步性而 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。从生成器内部的代码逻辑的角度来说,我们并不关心这个实现细节,这一点是非常强大的!
于是,request(..) 可能是以下两者之一:
// promisory request(..) (参见第3章) var request = Promise.wrap( ajax ); // vs. // thunkory request(..) var request = thunkify( ajax );
最后,作为前面 run(..) 工具的一个支持 thunk 的补丁,我们还需要这样的逻辑:
// .. // 我们收到返回的thunk了吗? else if (typeof next.value == "function") { return new Promise( function(resolve,reject){ // 用error-first回调调用这个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 ); } ); }
现在,我们的生成器可以调用 promisory 来 yield Promise,也可以调用 thunkory 来 yield thunk。不管哪种情况,run(..) 都能够处理这个值,并等待它的完成来恢复生成器运行。
从对称性来说,这两种方案看起来是一样的。但应该指出,这只是从代表生成器的未来值 continuation 的 Promise 或 thunk 的角度说才是正确的。
从更大的角度来说,thunk 本身基本上没有任何可信任性和可组合性保证,而这些是 Promise 的设计目标所在。单独使用 thunk 作为 Pormise 的替代在这个特定的生成器异步模式里是可行的,但是与 Promise 具备的优势(参见第 3 章)相比,这应该并不是一种理想方案。
如果可以选择的话,你应该使用 yield pr 而不是 yield th 。但对 run(..) 工具来说,对两种值类型都能提供支持则是完全正确的。
我的 asynquence 库(详见附录 A)中的 runner(..) 工具可以处理 Promise、thunk 和 asynquence 序列的 yield 。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论