1. var x = 1;
  2. function f(m){
  3. return m * 2;
  4. }
  5. f(x + 5)

调用上面的函数:

  1. //传值调用 js属于传值调用
  2. f(x + 5)
  3. // 传值调用时,等同于
  4. f(6)
  5. //传名调用
  6. f(x + 5)
  7. // 传名调用时,等同于
  8. (x + 5) * 2

编译器的”传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数

通过下面的例子加强理解:

  1. // 正常readFile函数
  2. fs.readFile(fileName, callback);
  3. // thunk函数处理后的readFile
  4. var readFileThunk = Thunk(fileName); // 将参数放到一个临时函数。这个readFileThunk就是thunk函数
  5. readFileThunk(callback); // 只接受回调作为参数
  6. var Thunk = function (fileName){
  7. return function (callback){
  8. return fs.readFile(fileName, callback);
  9. };
  10. };

理解下上面的thunk转化:
1、传统的readFile函数由一个多参数的函数 => 一个单参函数,只接受回调作为参数thunk函数其实是柯里化的一个表现。
2、从表现形式上看,其实就是把一个带有回调的传统函数的执行参数和回调分成两个函数。因此只要有回调,就可以用thunk转换

在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。个单参数版本,就叫做 Thunk 函数。

任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。下面是一个简单的 Thunk 函数转换器。

  1. var Thunk = function(fn){
  2. return function (){
  3. var args = Array.prototype.slice.call(arguments); // 缓存除回调以外的其他参数
  4. return function (callback){ // 返回的这个函数就是thunk函数
  5. args.push(callback); //把其他参数跟回调再合并
  6. return fn.apply(this, args); // 传入合并后的参数,函数真正调用的时候
  7. }
  8. };
  9. };

使用上面的转换器,生成 fs.readFile 的 Thunk 函数。这里面其实是柯里化的思想。

  1. var readFileThunk = Thunk(fs.readFile);
  2. readFileThunk(fileA)(callback); // 连续两次调用

thunkify模块

  1. var thunkify = require('thunkify');
  2. var fs = require('fs');
  3. var read = thunkify(fs.readFile);
  4. read('package.json')(function(err, str){
  5. // ...
  6. });

Thunkify源码:

  1. function thunkify(fn){
  2. return function(){
  3. var args = new Array(arguments.length);
  4. var ctx = this;
  5. for(var i = 0; i < args.length; ++i) {
  6. args[i] = arguments[i];
  7. }
  8. return function(done){
  9. var called;
  10. args.push(function(){
  11. if (called) return;
  12. called = true;
  13. done.apply(null, arguments);
  14. });
  15. try {
  16. fn.apply(ctx, args);
  17. } catch (err) {
  18. done(err);
  19. }
  20. }
  21. }
  22. };

它的源码主要多了一个检查机制,变量 called 确保回调函数只运行一次。这样的设计与下文的 Generator 函数相关。请看下面的例子。

  1. function f(a, b, callback){
  2. var sum = a + b;
  3. callback(sum);
  4. callback(sum);
  5. }
  6. var ft = thunkify(f);
  7. ft(1, 2)(console.log);
  8. // 3 只输出一次

thunk与generator结合:

generator函数封装异步读取文件:

  1. var fs = require('fs');
  2. var thunkify = require('thunkify');
  3. var readFile = thunkify(fs.readFile);
  4. var gen = function* (){
  5. var r1 = yield readFile('/etc/fstab'); // 这里的readFile执行后,返回的是一个thunk函数,只接受回调函数作为参数
  6. console.log(r1.toString());
  7. var r2 = yield readFile('/etc/shells');
  8. console.log(r2.toString());
  9. };

手动执行上面的函数:

  1. var it = gen();
  2. var r1 = it.next();
  3. r1.value(function(err, data){ // r1.value 就是第一个readFile执行后返回的一个thunk函数,只接受回调函数作为参数
  4. if (err) throw err;
  5. var r2 = it.next(data); // 执行下一个yield。it.next的参数会被当作上一个yield表达式的返回值。否则上面的r1将是undefined
  6. r2.value(function(err, data){
  7. if (err) throw err;
  8. it.next(data); // 这个data的值会赋给r2
  9. });
  10. });

仔细查看上面的代码,可以发现 Generator 函数的执行过程,其实是将同一个回调函数,反复传入 next 方法的 value 属性。这使得我们可以用递归来自动完成这个过程。那如何使用递归实现呢?

Thunk函数自动流程管理

Thunk 函数真正的威力,在于可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器。

  1. function run(gen) {
  2. var it = gen();
  3. function next(err, data) {
  4. var result = it.next(data);
  5. if (result.done) return;
  6. result.value(next); // result.value就是thunk函数
  7. }
  8. next();
  9. }
  10. run(gen);

上面代码的 run 函数,就是一个 Generator 函数的自动执行器。内部的 next 函数就是 Thunk 的回调函数。 next 函数先将指针移到 Generator 函数的下一步(gen.next 方法),然后判断 Generator 函数是否结束(result.done 属性),如果没结束,就将 next 函数再传入 Thunk 函数(result.value 属性),否则就直接退出。

有了这个执行器,执行 Generator 函数方便多了。不管有多少个异步操作,直接传入 run 函数即可。当然,前提是每一个异步操作,都要是 Thunk 函数,也就是说,跟在 yield 命令后面的必须是 Thunk 函数。redux-saga的实现原理貌似就是基于这个run逻辑来实现的? 这里面都有柯里化的思想。多个参数的函数转成单个参数的函数。

原文地址:http://www.ruanyifeng.com/blog/2015/05/thunk.html