返回介绍

第一部分 类型和语法

第二部分 异步和性能

4.7 形实转换程序

发布于 2023-05-24 16:38:21 字数 6617 浏览 0 评论 0 收藏 0

目前为止,我们已经假定从生成器 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文