Node.js的thunk函数&柯里化
前言
这是我在学习yield
异步编程的中遇到的thunk函数的一些理解,借此机会把thunk函数&柯里化&yield学习的一些心得记录一下。
thunk函数
在解释函数柯里化之前,先引入一个thunk
函数的概念,先来看一段代码
var x = 1
function f(x) {
return x * 2
}
console.log(f(x + 5))
针对如上这段代码的执行,有两种解释方式:
- 先计算出
x + 5
的值,然后再将该值传入函数f
做计算,这种被称为传值计算 - 另外一种是直接传入表达式
x + 5
,此时并没有做值计算,在最后计算(x + 5) * 2
时,才代入值计算,这种方式被称为传名计算
在不同的程序语言中有不同的处理方法,两种方式也是各有利弊,例如针对传值计算
function f2(x, y){
return y + 5
}
f2(2 * 4 + 4 + 14, 14)
经过复杂计算以后传入的值x,但在f2函数中并没有用到,这样就浪费了计算性能
thunk
函数就是一种传名计算,因为在Node.js中函数是一等公民,可以将一些函数参数先放入一个临时函数中,在将这个临时函数传入功能函数中做计算,这个临时函数就被称为thunk
函数,例如对如上的代码设计thunk
函数
var x = 1
function f(x) {
return x * 2
}
console.log(f(x + 5))
const thunkify = (x) => {
return x + 5
}
console.log(f(thunkify(x)))
thunkify
函数就是一个临时函数,将变量x传入临时函数中,然后再将临时函数thunkify
传入功能函数f
中进行计算。
柯里化
柯里化简单来说就是将一堆参数和一个函数绑定,然后形成一个新的函数的过程,在Node.js中主要又是针对异步函数的处理,主要用于将异步函数的普通调用参数和回调函数分离,例如针对异步读文件函数fs.readFile
,经thunk
处理以后
var fileName = 'bin.js'
// 原异步函数
fs.readFile(fileName, 'utf-8', (data, err) => {
console.log(data)
})
function thunkify(fileName) {
return function (cb) {
fs.readFile(fileName, 'utf-8', cb)
}
}
// 经thunk化的函数
thunkify(fileName)((err, data) => {
console.log(data)
})
此时注意代码
return function (cb) {
fs.readFile(fileName, 'utf-8', cb)
}
这里表示的是直接返回一个function
,并没有执行代码fs.readFile(fileName, 'utf-8', cb)
,同样的我们可以对一个任意数量参数的函数进行thunk
化
function thunkify2(fn){
return function () {
var args = [].slice.call(arguments)
return function (cb){
fn.apply(this, args.concat(cb))
}
}
}
thunkify3(fs.readFile)(fileName, 'utf-8')((err, data) => {
console.log(data)
})
thunkify3(fs.readFile)
首先返回一个临时函数,这个临时函数接收出回调函数以外的其他变量参数,然后再返回临时函数2,临时函数2会将接收的cb也就是回调函数和先前的变量参数拼接起来,然后执行thunk
化函数fn,这样就完成了对一个任意异步函数的thunk化。
为什么要使用thunk函数呢?
可能看到这里,会有这样的疑问,“为什么要使用thunk函数呢?我直接把参数和回调函数放一起调用不是一样吗?”,单独看起来thunk
是没有多大的作用,thunk
主要和yield
配合一起使用,在callback回调函数的异步编程方法提出以后,大多数编程人员为了避免回调地狱(多层回调)的情况,提出了后来几种改进方法,其中包括Promise、yield、await/async,这里我们就简单将yield是一种为了避免回调地狱的新异步编程方法,我们简单用yield
来实现斐波那契数列
function *fibonacci() {
var a = 0
var b = 1
while (true){
var c = a
a = b
b = c + a
yield a
}
}
var a = fibonacci()
console.log(a.next())
console.log(a.next())
console.log(a.next())
yield
相当于是一个状态暂定键,虽然在fibonacci函数中是一个死循环,但是处理到一个yield
时就将函数暂定运行,然后返回yield
后面的值,这里直观来看yield
并没有涉及到异步编程,yield
能提供异步编程功能是用自己这个状态暂停的特性结合Promise来实现的,工业使用上有co
库来完成这个功能,这里我们先不表yield
和Promise的结合,继续讲thunk
,那假如我要yield
来实现异步文件读取功能呢,那或许我会这么写代码
const fs = require('fs')
function *yieldFs() {
yield fs.readFile('bin.js', 'utf-8')
}
var data = yieldFs()
console.log(data.next())
符合我的直观感觉,yield异步读取文件之后,将读取的数据返回,但我们可以明显看到如上代码存在的问题,fs.readfile
是一个异步函数,在没有指定回调函数的情况下肯定是执行错误的,那假如我把代码又改成这样
const fs = require('fs')
function *yieldFs() {
yield fs.readFile('bin.js', 'utf-8', (err, data) => {
console.log(data)
})
}
var data = yieldFs()
console.log(data.next())
那我岂不是又掉入了回调陷阱?而且这本质上是没有用到yield的功能的,还是用回调函数来处理的,那现在我们需要转换一下策略了,原本的想法是直接从yield返回我们异步读取的文件数据,但这样不可避免回调地狱,那我们可以像这样来处理,用yield返回一个绑定了fs.readFile
和除回调函数以外的其他参数(thunk化),然后在函数外来处理添加回调函数,就像这样
const fs = require('fs')
const thunkify = function (fn) {
return function () {
var args = [].slice.call(arguments)
console.log(args)
return function(cb){
fn.apply(null, args.concat(cb))
}
}
}
const readFile = thunkify(fs.readFile)
function *yieldFs() {
yield readFile('bin.js', 'utf-8')
}
var yieldObject = yieldFs()
var thunkReadFile = yieldObject.next().value
thunkReadFile((err, data) => {
console.log(data)
})
这样我们就利用thunk函数将一个异步函数的常规参数调用和回调函数的绑定分开了,yield
返回内返回thunk临时函数,然后在生成器函数外再添加回调函数来执行异步函数。看到这里你可能会有一些疑惑,“这是不是还有是回调函数么?这和直接使用回调函数有什么区别呢?”在我的理解中,优化回调地狱的方法,无论是Promise还是yield等,目的并不是为了取消回调函数,因为回调函数式是Node.js的异步API规定了的必要参数,而这些优化方法要做的只是让回调函数书写和看起来更方便和直观一些,目的并不是要取代掉回调函数。
总结
thunk理念上实现的是函数传名计算,在Node.js中thunk主要用于将回调函数从异步函数中剥离出来,也是为了更好地书写回调函数,但其实这里只是很粗略地讲到了用thunk来配合yield使用,后面会更深入地理解一下co库配合thunk函数和yield来做异步处理。