Thunk 函数是什么鬼

1,944 阅读3分钟

JS中的thunk函数

说起thunk,我们先来看一个简易 thunk 结合 ajax 的例子

const thunkify = function (fn) {
    return function (url) {
        return function (callback) {
            return fn(url, callback)
        }
    }
}

function ajax(url, cb) {
    $.ajax({
        url: url,
        success: cb
    });
}
const getData = thunkify(ajax)
const req1 = getData('https://jsonplaceholder.typicode.com/todos/1')
req1(function (data) {
    console.log(data)
}) // req1 接收一个回调函数为参数

这时候就有小伙伴要说了,这不是 curry 吗?怎么能叫 thunk?区别在于解决的问题不同,首先 两者都是符合函数式编程理念的, thunk 是延迟计算,他将多参数改为单参数版本,且只接收回调函数,或是最后一个参数必需是回调函数,而 curry 却无该限制。thunk 更多是想把执行函数和回调函数拆分开,让开发者在写异步代码的时候可以更专注于执行函数的逻辑。

Thunkify 模块

建议使用更完善的 thunkify 转换器生成 thunk 函数 Thunkify 模块

使用方法

const ajaxThunk = thunkify(ajax);
ajaxThunk('https://jsonplaceholder.typicode.com/todos/1')(foo)

源码

function thunkify(fn) {
    return function () {
        var args = new Array(arguments.length);
        var ctx = this;
        for (var i = 0; i < args.length; ++i) {
            args[i] = arguments[i];
        }
        return function (done) {
            var called;
            args.push(function () {
                if (called) return;
                called = true;
                done.apply(null, arguments);
            });
            try {
                fn.apply(ctx, args);
            } catch (err) {
                done(err);
            }
        }
    }
};

自定义逻辑怎么办

还是以ajax为例:

function ajax(url, cb) {
    $.ajax({
        url: url,
        success: cb
    });
}
// --------------thunk写法--------------
var getData = thunkify(ajax);
// 执行函数
var req1 = getData("https://jsonplaceholder.typicode.com/todos/1");
var req2 = getData("https://jsonplaceholder.typicode.com/todos/2");
var req3 = getData("https://jsonplaceholder.typicode.com/todos/3");
// 回调函数
req1(function(data1) {
	req2(function(data2) {
		req3(function (data3) {
		})
	})
})
// --------------thunk写法--------------


// ---------------传统写法-----------------
ajax('./api/1', function(data1) {
	//传统定义的逻辑一般写在回调里
	ajax('./api/2', function(data2) {
		ajax('./api/3', function(data3) {
		})
	})
})
// ---------------传统写法-----------------

根据 thunk 的理念我们将执行函数回调函数分开以后,代码清晰了许多。 但是问题来了 自定义的逻辑 应该放在哪?

如果自定义的逻辑是放在回调函数集合那边,嵌套逻辑处理太多的话,那thunk的优势就没了,且可读性大大降低,那我们就把自定义的逻辑放在执行函数的一端,回调函数只是负责获取数据,并将参数传回执行函数集合

所以现在流程就是

执行函数-->等待回调函数返回数据-->对数据进行自定义操作

等待传回数据不就是 Gennerator 函数吗?Thunk 函数确实没什么卵用,但真正让其发挥作用的是配合Generator函数实现自动化异步操作。

如何结合

把所有的执行函数放入generator函数里面,利用generator函数的yield对执行函数的流程控制再把函数执行权移出函数到对应的回调函数,获取数据后再把数据返回来。

还是以ajax请求为例

我们利用 thunk执行回调 拆分开 从而变成执行函数放在一起 回调函数放在一起, 并且使用 yield 将两者进行连接

import $ from "jquery";
import thunkify from "thunkify";

function ajax(url, cb) {
  $.ajax({
    url: url,
    success: cb
  });
}

const getData = thunkify(ajax);

// 创建 Generator
const gen = function* () {
  const data1 = yield getData("https://jsonplaceholder.typicode.com/todos/1");
  console.log(data1); // 获取数据后自定义操作
  const data2 = yield getData("https://jsonplaceholder.typicode.com/todos/2");
  console.log(data2);
  const data3 = yield getData("https://jsonplaceholder.typicode.com/todos/3");
  console.log(data3);
};

// 手动执行 Generator
var g = gen();
var data1 = g.next();
// 执行回调函数传回数据
data1.value(function (data) {
  const data2 = g.next(data);
  data2.value(function (data2) {
    const data3 = g.next(data2);
    data3.value(function (data3) {
      g.next(data3);
    });
  });
});

该函数形式单一,基本形式为

d.value(function(data) {
 	g.next(data);
 })

可以利用递归写一个 Thunk自动执行器

function run(fn) {
	var g = fn();
	function next(data) {
		var result = g.next(data);
		if (result.done) return;
		result.value(next);
	}
	next();
}
run(gen);

在线代码演示:codesandbox

定义了run方法后,直接将Generator函数作为参数传给run就可以了,函数像多米诺骨牌一样依次执行Generator函数内的异步操作。当然,前提是每一个yield命令后面的必须是Thunk函数。

Thunk函数是自动执行Generator函数的一种选择,或者可以像上文那样定义run函数,同样可以使Generator函数自动执行。

应用

看到这里,如果您接触过Redux-Saga,你会发现似曾相识也会更好的帮你理解saga


参考:
What is the difference between Thunk and currying in JavaScript?
Are thunk and function currying the same?
维基百科
阮一峰
yongningfu