跳到主要内容

异步编程中的 thunk 函数

什么是Thunk

Thunk英译即转化程序,第一次遇见这个名词是在使用redux-thunk的时候,只知道是做为异步和同步中间件来使用,并未深入的了解,接下来发现koa的框架中是因在koa1的概念中设计到了它,故学习了一番,因此做一个简单的总结。

写一个简单的函数,用于读取package.json的信息:

fs.readFile('package.json', (err, data) => {
if (err) throw err;
console.log(data.toString())
})

在某些情况下,我们需要将参数进行包装一下,比如在koa1的生成器中间件中,我们必须将函数包装成有且只有一个callback函数,这样koa中间件才能识别函数:

const app = new Koa()

app.use(function*(next) {
const data = yield readFileThunk('package.json')
console.log(data)
yield next
})

function readFileThunk(path, cb) {
return function(cb) {
fs.readFile(path, (err, data) => {
if (err) throw err
cb(null, data.toString())
})
}
}
app.listen(3000)

通过访问端口,我们看到了正确的打印信息,将正常函数包装为上述函数,即称之为thunk函数。Thunk函数将多参数函数替换成了单参数版本,与高阶函数、柯里化思想类似。播个小插曲,在koa2中,中间件写法更迭成了async/await,其原理也是基于generator进行又一次封装,代码为:

app.use(async(ctx, next) => {
const data = await readFileWrap('package.json')
console.log(data)
await next()
})

function readFileWrap(path, cb) {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if (err) throw reject(error)
resolve(data.toString())
})
})
}

我们接下来自己实现一个简易的thunk工具方法,思路如下:

  1. 一共三个执行函数,第一层函数传入主函数方法fn,并返回一个带有fn方法的闭包
  2. 第二层函数传入参数args,然后返回一个仅需传入回调函数供外部执行的方法
  3. 第三层函数传入一个回调函数cb, 然后执行主程序
function thunkit(fn) {
return function() {
var args = Array.prototype.slice.call(arguments)
return function(cb) {
args.push(cb)
return fn.apply(this, args)
}
}
}

在实际使用中,往往业务场景更复杂,因此推荐使用node-thunkify库,源代码也只有28行,请看下一节。

thunkify源码解析

function thunkify(fn){
assert('function' == typeof fn, 'function required'); // 是否为函数

return function(){
var args = new Array(arguments.length);
var ctx = this; // 绑定函数的上下文对象

// 初始化了一个新的数组,这种写法兼容性更强,不像本文上面直接用的.slice方法进行拷贝数组
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); // 返回错误
}
}
}
};

源代码已经够简单了,想也无需多说明,重要的是函数本身的思想与灵活的去运用

参考资料