对阮一峰老师的thunk函数一节个人的补充理解
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定义的函数来说,用户自定义的逻辑到底是放在 回调函数的集合 这边还是放在 执行函数集合 那边
如果用户自定义的逻辑是放在回调函数集合那边, 有两个缺点
- 代码逻辑里面上面来说不符合常规逻辑
- 回调函数里面嵌套逻辑处理太多的话,那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);
thunk的重点在于理解参数的执行时机,
@captainblue2013 上面不是说了么 chunk就是形式上把 执行参数 和 回调分开 利用yield把回调里面获取的数据传回 执行参数函数 用户自定义的逻辑处理就可以放在执行函数那边处理 而回调函数单独管理 你说的也没错 我只是说的详细一点罢了
我觉得做这么多都是为解决回调黑洞的问题,现在这个问题可以是基本解决了,Es7 async之后可以说是最终解决方案了
@yongningfu 当然不只是形式上,你再想想
@huangyanxiong01 嗯嗯 async也是在这些基础上面解决的 前面的基础还是需要了解的
本质上就是 把两者分开 不知道你还想说什么?
为什么我这么说呢, 你想想回调函数麻烦在哪里呢? 数据和执行函数获取不同步 需要回调函数才能获取数据
算了 我还是展开的说一下吧
上面文章中 我已经把一个异步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 ,你说的都是表面看上去的变化, 实际最精妙的还是求值策略的差异。