对阮一峰老师的thunk函数一节个人的补充理解
发布于 8 年前 作者 yongningfu 8109 次浏览 来自 分享

对阮一峰老师的thunk函数一节个人的补充理解

ECMAScript 6 入门

thunk函数的作用非常简单 其实就是把一个函数的 执行参数 和 回调分成两个函数(可以把函数本身也进行包装) 只要是有回调函数的 就可以用thunk进行包装

比如thunk把

fn(args, callback)--->变成--->thunk(fn)(args)(callback)

thunkify的代码

function thunkify(fn) {
	return function() {
		var args = Array.prototype.slice.call(arguments);
		var ctx = this;

		return function(done) {
			var called = false;
			args.push(function() {
				if (called) return;
				called = true;
				done.apply(null, arguments);
			});
			try {
				fn.apply(ctx, args);
			} catch (err) {
				done(err);
			}
		}
	}
}

这有什么用呢?

因为thunk 形式上 把回调函数和执行函数分开,所以我们可以做到在 一个地方执行执行函数,其他地方执行回函数, 而且对于很对异步操作来说, 想想, 那我们是不分可以把这些执行函数全部放在一块执行, 他们对应的回调函数放在其他地方执行,而且只是需要确定回调函数执行的顺序和嵌套关系, 就能取得执行函数的返回顺序

用代码解释一下上面的信息

var fs = require('fs');
var readFile = thunkify(fs.readFile);

//这是执行函数集合
var f1 = readFile('./a.js');
//用户自定义的逻辑在这?
var f2 = readFile('./b.js');
var f3 = readFile('./c.js');



//这是回调函数集合

//利用嵌套控制f1 f2执行的顺序
f1(function(err, data1) {
	//还是用户定义的逻辑在这?
	f2(function(err, data2) {

		f3(function (err, data3) {

		})
	})
})


//传统写法
fs.readFile('./a.js', function(err, data1) {
	//传统用户定义的逻辑在这
	fs.readFile('./b.js', function(err, data2) {
		fs.readFile('./c.js', function(err, data3) {

		})
	})
})


可以看到把执行函数和回调函数分开以后,代码清晰了许多 但是问题来了 用户自定义的逻辑 应该放在哪

首先先说一点 回调函数的一个作用就是获取数据的

那么对应thunk定义的函数来说,用户自定义的逻辑到底是放在 回调函数的集合 这边还是放在 执行函数集合 那边

如果用户自定义的逻辑是放在回调函数集合那边, 有两个缺点

  1. 代码逻辑里面上面来说不符合常规逻辑
  2. 回调函数里面嵌套逻辑处理太多的话,那thunk的优势就没了

那就确定了把用户自定义的逻辑放在执行函数的基本一端,回调函数只是负责获取数据,并在数据传回执行函数集合

所以现在的基本流程就是

执行函数执行-->等待回调函数传回数据-->用户对于获取的数据进行操作

等待传回数据是不是想到了 gennerator yield

所以就有了thunkify和generator的完美结合

如何结合?

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

利用fs.readFile举例子

利用thunk把fs.readFile(arguments, callback) 执行的参数和回调函数分开 从而变成 执行函数放在一起 回调函数放在一起 利用yield进行连接

var fs = require('fs');
var readFile = thunkify(fs.readFile);

//发现执行参数的函数在一起
var gen = function* () {
	var data1 = yield readFile('./a.js');
	//用户获取数据后自定义写在这里
	console.log(data1.toString());
	var data2 = yield readFile('./b.js');
		//用户获取数据后自定义写在这里
	console.log(data2);
}


//写个执行函数
//发现callback在一起  而且调用的形式都一样
var g = gen();
var d1 = g.next();

//执行value 实际为执行总函数 -->回调函数
d1.value(function(err, data) {
	if (err) throw err;
	//传回数据
	var d2 = g.next(data);
	d2.value(function(err, data2) {
		if (err) throw err;
		g.next(data2);
	});
});

发现上面的g的执行形式单一

基本形式为

d.value(function(err, data) {
 	if (err) throw err;
 	g.next(data);
 })

可以利用递归写一个run函数 每个下一个都只和回调函数 callback(err ,data)有关 提取callback(err, data)

function run(fn) {

	var g = fn();
	//下一步----实际就是回调函数
	function next(err, data) {
		//把前面一个数据给传递到gen()函数里面
		var result = g.next(data);
		//判断是否结束
		if (result.done) return;
		//下一句执行回调next的时候 不断的递归
		result.value(next);
	}
	//执行第一步
	next();
}

run(gen);
7 回复

thunk的重点在于理解参数的执行时机,

@captainblue2013 上面不是说了么 chunk就是形式上把 执行参数 和 回调分开 利用yield把回调里面获取的数据传回 执行参数函数 用户自定义的逻辑处理就可以放在执行函数那边处理 而回调函数单独管理 你说的也没错 我只是说的详细一点罢了

我觉得做这么多都是为解决回调黑洞的问题,现在这个问题可以是基本解决了,Es7 async之后可以说是最终解决方案了

@yongningfu 当然不只是形式上,你再想想

@huangyanxiong01 嗯嗯 async也是在这些基础上面解决的 前面的基础还是需要了解的

@captainblue2013

本质上就是 把两者分开 不知道你还想说什么?

为什么我这么说呢, 你想想回调函数麻烦在哪里呢? 数据和执行函数获取不同步 需要回调函数才能获取数据

算了 我还是展开的说一下吧

上面文章中 我已经把一个异步api分成了 执行函数 和 回调函数

exec(args, callback)
//执行函数是 exec  回调函数是 callback

为何这么分呢? 因为我们真正执行想要的是exec 回调函数是需要为
了获取数据的而附加执行的

传统同步编程来说数据和 “执行函数” 执行就可以理解获取数据 所以我们编程写法为

//假设的同步(同步写法)
var data1 = fs.readFileSync('./a.js');
var data2 = fs.readFileSync('./b.js');

这个同步的看起来很好理解

所以异步的同学就想了 有没有编程的时候只是关注 “执行函数” 呢? 要解决这个问题 就必须要处理好 “回调函数” 的这个部分 所以就有了 执行函数 和 回调函数的分家

// fs.readFile(args, callback)

//利用thunk 分为执行函数  回调函数
var readFile = thunkify(fs.readFile); //执行函数
var wrapCallback =  readFile('./a.js'); //被包装的回调函数
//一看 好像执行函数 和  回调函数还是没分家 因为你还没适应generator

{   //这个是一个执行函数执行块
 		var data = yield readFile('./a.js');
 		          yield readFile('./b.js');
}


{	//这个是一个回调函数执行块  
	wrapCallback(a.js)
	wrapCallback(b.js)
}

//所以你可以看到  两者其实实现分家了  回调函数可以自动执行 所以我们只需要关心  执行函数  行了

我讲的是思想 就是thunk函数为何这么设计 至于内部做了什么 thunkify代码也比较好理解,这里就不解释了

@yongningfu 我说的重点是 求值策略, call by value & call by name ,你说的都是表面看上去的变化, 实际最精妙的还是求值策略的差异。

回到顶部