第四章: Generator - Thunks

优质
小牛编辑
127浏览
2023-12-01

Thunks

至此,我们都假定从一个generator中yield一个Promise——让这个Promise使用像run(..)这样的帮助工具来推进generator——是管理使用generator的异步处理的最佳方法。明白地说,它是的。

但是我们跳过了一个被轻度广泛使用的模式,为了完整性我们将简单地看一看它。

在一般的计算机科学中,有一种老旧的前JS时代的概念,称为“thunk”。我们不在这里赘述它的历史,一个狭隘的表达是,thunk是一个JS函数——没有任何参数——它连接并调用另一个函数。

换句话讲,你用一个函数定义包装函数调用——带着它需要的所有参数——来 推迟 这个调用的执行,而这个包装用的函数就是thunk。当你稍后执行thunk时,你最终会调用那个原始的函数。

举个例子:

  1. function foo(x,y) {
  2. return x + y;
  3. }
  4. function fooThunk() {
  5. return foo( 3, 4 );
  6. }
  7. // 稍后
  8. console.log( fooThunk() ); // 7

所以,一个同步的thunk是十分直白的。但是一个异步的thunk呢?我们实质上可以扩展这个狭隘的thunk定义,让它接收一个回调。

考虑这段代码:

  1. function foo(x,y,cb) {
  2. setTimeout( function(){
  3. cb( x + y );
  4. }, 1000 );
  5. }
  6. function fooThunk(cb) {
  7. foo( 3, 4, cb );
  8. }
  9. // 稍后
  10. fooThunk( function(sum){
  11. console.log( sum ); // 7
  12. } );

如你所见,fooThunk(..)仅需要一个cb(..)参数,因为它已经预先制定了值34(分别为xy)并准备传递给foo(..)。一个thunk只是在外面耐心地等待着它开始工作所需的最后一部分信息:回调。

但是你不会想要手动制造thunk。那么,让我们发明一个工具来为我们进行这种包装。

考虑这段代码:

  1. function thunkify(fn) {
  2. var args = [].slice.call( arguments, 1 );
  3. return function(cb) {
  4. args.push( cb );
  5. return fn.apply( null, args );
  6. };
  7. }
  8. var fooThunk = thunkify( foo, 3, 4 );
  9. // 稍后
  10. fooThunk( function(sum) {
  11. console.log( sum ); // 7
  12. } );

提示: 这里我们假定原始的(foo(..))函数签名希望它的回调的位置在最后,而其它的参数在这之前。这是一个异步JS函数的相当普遍的“标准”。你可以称它为“回调后置风格”。如果因为某些原因你需要处理“回调优先风格”的签名,你只需要制造一个使用args.unshift(..)而非args.push(..)的工具。

前面的thunkify(..)公式接收foo(..)函数的引用,和任何它所需的参数,并返回thunk本身(fooThunk(..))。然而,这并不是你将在JS中发现的thunk的典型表达方式。

thunkify(..)制造thunk本身相反,典型的——可能有点儿让人困惑的——thunkify(..)工具将产生一个制造thunk的函数。

额…是的。

考虑这段代码:

  1. function thunkify(fn) {
  2. return function() {
  3. var args = [].slice.call( arguments );
  4. return function(cb) {
  5. args.push( cb );
  6. return fn.apply( null, args );
  7. };
  8. };
  9. }

这里主要的不同之处是有一个额外的return function() { .. }。这是它在用法上的不同:

  1. var whatIsThis = thunkify( foo );
  2. var fooThunk = whatIsThis( 3, 4 );
  3. // 稍后
  4. fooThunk( function(sum) {
  5. console.log( sum ); // 7
  6. } );

明显地,这段代码隐含的最大的问题是,whatIsThis叫什么合适?它不是thunk,它是一个从foo(..)调用生产thunk的东西。它是一种“thunk”的“工厂”。而且看起来没有任何标准的意见来命名这种东西。

所以,我的提议是“thunkory”(”thunk” + “factory”)。于是,thunkify(..)制造了一个thunkory,而一个thunkory制造thunks。这个道理与第三章中我的“promisory”提议是对称的:

  1. var fooThunkory = thunkify( foo );
  2. var fooThunk1 = fooThunkory( 3, 4 );
  3. var fooThunk2 = fooThunkory( 5, 6 );
  4. // 稍后
  5. fooThunk1( function(sum) {
  6. console.log( sum ); // 7
  7. } );
  8. fooThunk2( function(sum) {
  9. console.log( sum ); // 11
  10. } );

注意: 这个例子中的foo(..)期望的回调不是“错误优先风格”。当然,“错误优先风格”更常见。如果foo(..)有某种合理的错误发生机制,我们可以改变而使它期望并使用一个错误优先的回调。后续的thunkify(..)不会关心回调被预想成什么样。用法的唯一区别是fooThunk1(function(err,sum){..

暴露出thunkory方法——而不是像早先的thunkify(..)那样将中间步骤隐藏起来——可能看起来像是没必要的混乱。但是一般来讲,在你的程序一开始就制造一些thunkory来包装既存API的方法是十分有用的,然后你就可以在你需要thunk的时候传递并调用这些thunkory。这两个区别开的步骤保证了功能上更干净的分离。

来展示一下的话:

  1. // 更干净:
  2. var fooThunkory = thunkify( foo );
  3. var fooThunk1 = fooThunkory( 3, 4 );
  4. var fooThunk2 = fooThunkory( 5, 6 );
  5. // 而这个不干净:
  6. var fooThunk1 = thunkify( foo, 3, 4 );
  7. 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(..)的例子改为假定一个“错误优先风格”回调的形式:

  1. function foo(x,y,cb) {
  2. setTimeout( function(){
  3. // 假定 `cb(..)` 是“错误优先风格”
  4. cb( null, x + y );
  5. }, 1000 );
  6. }

现在,我们将比较thunkify(..)promisify(..)(也就是第三章的Promise.wrap(..)):

  1. // 对称的:构建问题的回答者
  2. var fooThunkory = thunkify( foo );
  3. var fooPromisory = promisify( foo );
  4. // 对称的:提出问题
  5. var fooThunk = fooThunkory( 3, 4 );
  6. var fooPromise = fooPromisory( 3, 4 );
  7. // 取得 thunk 的回答
  8. fooThunk( function(err,sum){
  9. if (err) {
  10. console.error( err );
  11. }
  12. else {
  13. console.log( sum ); // 7
  14. }
  15. } );
  16. // 取得 promise 的回答
  17. fooPromise
  18. .then(
  19. function(sum){
  20. console.log( sum ); // 7
  21. },
  22. function(err){
  23. console.error( err );
  24. }
  25. );

thunkory和promisory实质上都是在问一个问题(一个值),thunk的fooThunk和promise的fooPromise分别代表这个问题的未来的答案。这样看来,对称性就清楚了。

带着这个视角,我们可以看到为了异步而yieldPromise的generator,也可以为异步而yieldthunk。我们需要的只是一个更聪明的run(..)工具(就像以前一样),它不仅可以寻找并连接一个被yield的Promise,而且可以给一个被yield的thunk提供回调。

考虑这段代码:

  1. function *foo() {
  2. var val = yield request( "http://some.url.1" );
  3. console.log( val );
  4. }
  5. run( foo );

在这个例子中,request(..)既可以是一个返回一个promise的promisory,也可以是一个返回一个thunk的thunkory。从generator的内部代码逻辑的角度看,我们不关心这个实现细节,这就它强大的地方!

所以,request(..)可以使以下任何一种形式:

  1. // promisory `request(..)` (见第三章)
  2. var request = Promise.wrap( ajax );
  3. // vs.
  4. // thunkory `request(..)`
  5. var request = thunkify( ajax );

最后,作为一个让我们早先的run(..)工具支持thunk的补丁,我们可能会需要这样的逻辑:

  1. // ..
  2. // 我们收到了一个回调吗?
  3. else if (typeof next.value == "function") {
  4. return new Promise( function(resolve,reject){
  5. // 使用一个错误优先回调调用thunk
  6. next.value( function(err,msg) {
  7. if (err) {
  8. reject( err );
  9. }
  10. else {
  11. resolve( msg );
  12. }
  13. } );
  14. } )
  15. .then(
  16. handleNext,
  17. function handleErr(err) {
  18. return Promise.resolve(
  19. it.throw( err )
  20. )
  21. .then( handleResult );
  22. }
  23. );
  24. }

现在,我们generator既可以调用promisory来yieldPromise,也可以调用thunkory来yieldthunk,而不论那种情况,run(..)都将处理这个值并等待它的完成,以继续generator。

在对称性上,这两个方式是看起来相同的。然而,我们应当指出这仅仅从Promise或thunk表示延续generator的未来值的角度讲是成立的。

从更高的角度讲,与Promise被设计成的那样不同,thunk没有提供,它们本身也几乎没有任何可靠性和可组合性的保证。在这种特定的generator异步模式下使用一个thunk作为Promise的替代品是可以工作的,但与Promise提供的所有好处相比,这应当被看做是一种次理想的方法。

如果你有选择,那就偏向yield pr而非yield th。但是使run(..)工具可以处理两种类型的值本身没有什么问题。

注意: 在我们将要在附录A中讨论的,我的 asynquence 库中的runner(..)工具,可以处理yield的Promise,thunk和 asynquence 序列。