让人头痛的Generator 函数的异步应用真的有用吗?( 三 )


一种意见是"传值调用"(call by value) , 即在进入函数体之前 , 就计算x + 5的值(等于 6) , 再将这个值传入函数f 。 C 语言就采用这种策略 。
f(x + 5)// 传值调用时 , 等同于f(6)另一种意见是“传名调用”(call by name) , 即直接将表达式x + 5传入函数体 , 只在用到它的时候求值 。 Haskell 语言采用这种策略 。
f(x + 5)// 传名调用时 , 等同于(x + 5) * 2传值调用和传名调用 , 哪一种比较好?
回答是各有利弊 。 传值调用比较简单 , 但是对参数求值的时候 , 实际上还没用到这个参数 , 有可能造成性能损失 。
function f(a, b){return b;}f(3 * x * x - 2 * x - 1, x);上面代码中 , 函数f的第一个参数是一个复杂的表达式 , 但是函数体内根本没用到 。 对这个参数求值 , 实际上是不必要的 。 因此 , 有一些计算机学家倾向于"传名调用" , 即只在执行时求值 。
Thunk 函数的含义编译器的“传名调用”实现 , 往往是将参数放到一个临时函数之中 , 再将这个临时函数传入函数体 。 这个临时函数就叫做 Thunk 函数 。
function f(m) {return m * 2;}f(x + 5);// 等同于var thunk = function () {return x + 5;};function f(thunk) {return thunk() * 2;}上面代码中 , 函数 f 的参数x + 5被一个函数替换了 。 凡是用到原参数的地方 , 对Thunk函数求值即可 。
这就是 Thunk 函数的定义 , 它是“传名调用”的一种实现策略 , 用来替换某个表达式 。
JavaScript 语言的 Thunk 函数JavaScript 语言是传值调用 , 它的 Thunk 函数含义有所不同 。 在 JavaScript 语言中 , Thunk 函数替换的不是表达式 , 而是多参数函数 , 将其替换成一个只接受回调函数作为参数的单参数函数 。
// 正常版本的readFile(多参数版本)fs.readFile(fileName, callback);// Thunk版本的readFile(单参数版本)var Thunk = function (fileName) {return function (callback) {return fs.readFile(fileName, callback);};};var readFileThunk = Thunk(fileName);readFileThunk(callback);上面代码中 , fs模块的readFile方法是一个多参数函数 , 两个参数分别为文件名和回调函数 。 经过转换器处理 , 它变成了一个单参数函数 , 只接受回调函数作为参数 。 这个单参数版本 , 就叫做 Thunk 函数 。
任何函数 , 只要参数有回调函数 , 就能写成 Thunk 函数的形式 。 下面是一个简单的 Thunk 函数转换器 。
// ES5版本var Thunk = function(fn){return function (){var args = Array.prototype.slice.call(arguments);return function (callback){args.push(callback);return fn.apply(this, args);}};};// ES6版本const Thunk = function(fn) {return function (...args) {return function (callback) {return fn.call(this, ...args, callback);}};};使用上面的转换器 , 生成fs.readFile的 Thunk 函数 。
var readFileThunk = Thunk(fs.readFile);readFileThunk(fileA)(callback);下面是另一个完整的例子 。
function f(a, cb) {cb(a);}const ft = Thunk(f);ft(1)(console.log) // 1Thunkify 模块生产环境的转换器 , 建议使用 Thunkify 模块 。
首先是安装 。
$ npm install thunkify使用方式如下 。
var thunkify = require('thunkify');var fs = require('fs');var read = thunkify(fs.readFile);read('package.json')(function(err, str){// ...});Thunkify 的源码与上一节那个简单的转换器非常像 。
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);}}}};它的源码主要多了一个检查机制 , 变量called确保回调函数只运行一次 。 这样的设计与下文的 Generator 函数相关 。 请看下面的例子 。
function f(a, b, callback){var sum = a + b;callback(sum);callback(sum);}var ft = thunkify(f);var print = console.log.bind(console);ft(1, 2)(print);// 3上面代码中 , 由于thunkify只允许回调函数执行一次 , 所以只输出一行结果 。
Generator 函数的流程管理你可能会问 ,Thunk 函数有什么用?回答是以前确实没什么用 , 但是 ES6 有了 Generator 函数 , Thunk 函数现在可以用于 Generator 函数的自动流程管理 。
Generator 函数可以自动执行 。
function* gen() {// ...}var g = gen();var res = g.next();while(!res.done){console.log(res.value);res = g.next();}上面代码中 , Generator 函数gen会自动执行完所有步骤 。
但是 , 这不适合异步操作 。 如果必须保证前一步执行完 , 才能执行后一步 , 上面的自动执行就不可行 。 这时 , Thunk 函数就能派上用处 。 以读取文件为例 。 下面的 Generator 函数封装了两个异步操作 。