003 Thunk 是什么鬼

7/19/2021 Javascript

# JS中的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 接收一个回调函数为参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

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

# Thunkify 模块

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

使用方法

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

源码

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);
            }
        }
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 自定义逻辑怎么办

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) {
		})
	})
})
// ---------------传统写法-----------------
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

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

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

所以现在流程就是

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

等待传回数据不就是 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);
    });
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

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

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

可以利用递归写一个 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);
1
2
3
4
5
6
7
8
9
10

codesandbox (opens new window)

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

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


参考: 维基百科-求值策略 (opens new window) 阮一峰 (opens new window) What is the difference between Thunk and currying in JavaScript? (opens new window) Are thunk and function currying the same? (opens new window) yongningfu (opens new window)

Last Updated: 8/7/2021, 11:34:32 PM