Thunk 函数的含义和用法
一、參數(shù)的求值策略
Thunk函數(shù)早在上個世紀(jì)60年代就誕生了。
那時,編程語言剛剛起步,計算機學(xué)家還在研究,編譯器怎么寫比較好。一個爭論的焦點是"求值策略",即函數(shù)的參數(shù)到底應(yīng)該何時求值。
var x = 1;function f(m){return m * 2; }f(x + 5)上面代碼先定義函數(shù) f,然后向它傳入表達(dá)式 x + 5 。請問,這個表達(dá)式應(yīng)該何時求值?
一種意見是"傳值調(diào)用"(call by value),即在進入函數(shù)體之前,就計算 x + 5 的值(等于6),再將這個值傳入函數(shù) f 。C語言就采用這種策略。
f(x + 5) // 傳值調(diào)用時,等同于 f(6)另一種意見是"傳名調(diào)用"(call by name),即直接將表達(dá)式 x + 5 傳入函數(shù)體,只在用到它的時候求值。Hskell語言采用這種策略。
f(x + 5) // 傳名調(diào)用時,等同于 (x + 5) * 2傳值調(diào)用和傳名調(diào)用,哪一種比較好?回答是各有利弊。傳值調(diào)用比較簡單,但是對參數(shù)求值的時候,實際上還沒用到這個參數(shù),有可能造成性能損失。
function f(a, b){return b; }f(3 * x * x - 2 * x - 1, x);上面代碼中,函數(shù) f 的第一個參數(shù)是一個復(fù)雜的表達(dá)式,但是函數(shù)體內(nèi)根本沒用到。對這個參數(shù)求值,實際上是不必要的。
因此,有一些計算機學(xué)家傾向于"傳名調(diào)用",即只在執(zhí)行時求值。
二、Thunk 函數(shù)的含義
編譯器的"傳名調(diào)用"實現(xiàn),往往是將參數(shù)放到一個臨時函數(shù)之中,再將這個臨時函數(shù)傳入函數(shù)體。這個臨時函數(shù)就叫做 Thunk 函數(shù)。
function f(m){return m * 2; }f(x + 5);// 等同于var thunk = function () {return x + 5; };function f(thunk){return thunk() * 2; }上面代碼中,函數(shù) f 的參數(shù) x + 5 被一個函數(shù)替換了。凡是用到原參數(shù)的地方,對 Thunk 函數(shù)求值即可。
這就是 Thunk 函數(shù)的定義,它是"傳名調(diào)用"的一種實現(xiàn)策略,用來替換某個表達(dá)式。
三、JavaScript 語言的 Thunk 函數(shù)
JavaScript 語言是傳值調(diào)用,它的 Thunk 函數(shù)含義有所不同。在 JavaScript 語言中,Thunk 函數(shù)替換的不是表達(dá)式,而是多參數(shù)函數(shù),將其替換成單參數(shù)的版本,且只接受回調(diào)函數(shù)作為參數(shù)。
// 正常版本的readFile(多參數(shù)版本) fs.readFile(fileName, callback);// Thunk版本的readFile(單參數(shù)版本) var readFileThunk = Thunk(fileName); readFileThunk(callback);var Thunk = function (fileName){return function (callback){return fs.readFile(fileName, callback); }; };上面代碼中,fs 模塊的 readFile 方法是一個多參數(shù)函數(shù),兩個參數(shù)分別為文件名和回調(diào)函數(shù)。經(jīng)過轉(zhuǎn)換器處理,它變成了一個單參數(shù)函數(shù),只接受回調(diào)函數(shù)作為參數(shù)。這個單參數(shù)版本,就叫做 Thunk 函數(shù)。
任何函數(shù),只要參數(shù)有回調(diào)函數(shù),就能寫成 Thunk 函數(shù)的形式。下面是一個簡單的 Thunk 函數(shù)轉(zhuǎn)換器。
var Thunk = function(fn){return function (){var args = Array.prototype.slice.call(arguments);return function (callback){args.push(callback);return fn.apply(this, args);}}; };使用上面的轉(zhuǎn)換器,生成 fs.readFile 的 Thunk 函數(shù)。
var readFileThunk = Thunk(fs.readFile); readFileThunk(fileA)(callback);四、Thunkify 模塊
生產(chǎn)環(huán)境的轉(zhuǎn)換器,建議使用?Thunkify 模塊。
首先是安裝。
$ npm install thunkify使用方式如下。
var thunkify = require('thunkify'); var fs = require('fs');var read = thunkify(fs.readFile); read('package.json')(function(err, str){// ... });Thunkify 的源碼與上一節(jié)那個簡單的轉(zhuǎn)換器非常像。
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?確保回調(diào)函數(shù)只運行一次。這樣的設(shè)計與下文的 Generator 函數(shù)相關(guān)。請看下面的例子。
function f(a, b, callback){var sum = a + b;callback(sum);callback(sum); }var ft = thunkify(f); ft(1, 2)(console.log); // 3上面代碼中,由于 thunkify 只允許回調(diào)函數(shù)執(zhí)行一次,所以只輸出一行結(jié)果。
五、Generator 函數(shù)的流程管理
你可能會問, Thunk 函數(shù)有什么用?回答是以前確實沒什么用,但是 ES6 有了 Generator 函數(shù),Thunk 函數(shù)現(xiàn)在可以用于 Generator 函數(shù)的自動流程管理。
以讀取文件為例。下面的 Generator 函數(shù)封裝了兩個異步操作。
var fs = require('fs'); var thunkify = require('thunkify'); var readFile = thunkify(fs.readFile);var gen = function* (){var r1 = yield readFile('/etc/fstab');console.log(r1.toString());var r2 = yield readFile('/etc/shells');console.log(r2.toString()); };上面代碼中,yield 命令用于將程序的執(zhí)行權(quán)移出 Generator 函數(shù),那么就需要一種方法,將執(zhí)行權(quán)再交還給 Generator 函數(shù)。
這種方法就是 Thunk 函數(shù),因為它可以在回調(diào)函數(shù)里,將執(zhí)行權(quán)交還給 Generator 函數(shù)。為了便于理解,我們先看如何手動執(zhí)行上面這個 Generator 函數(shù)。
var g = gen();var r1 = g.next(); r1.value(function(err, data){if (err) throw err;var r2 = g.next(data);r2.value(function(err, data){if (err) throw err;g.next(data);}); });上面代碼中,變量 g 是 Generator 函數(shù)的內(nèi)部指針,表示目前執(zhí)行到哪一步。next 方法負(fù)責(zé)將指針移動到下一步,并返回該步的信息(value 屬性和 done 屬性)。
仔細(xì)查看上面的代碼,可以發(fā)現(xiàn) Generator 函數(shù)的執(zhí)行過程,其實是將同一個回調(diào)函數(shù),反復(fù)傳入 next 方法的 value 屬性。這使得我們可以用遞歸來自動完成這個過程。
六、Thunk 函數(shù)的自動流程管理
Thunk 函數(shù)真正的威力,在于可以自動執(zhí)行 Generator 函數(shù)。下面就是一個基于 Thunk 函數(shù)的 Generator 執(zhí)行器。
function run(fn) {var gen = fn();function next(err, data) {var result = gen.next(data);if (result.done) return;result.value(next);}next(); }run(gen);上面代碼的 run 函數(shù),就是一個 Generator 函數(shù)的自動執(zhí)行器。內(nèi)部的 next 函數(shù)就是 Thunk 的回調(diào)函數(shù)。 next 函數(shù)先將指針移到 Generator 函數(shù)的下一步(gen.next 方法),然后判斷 Generator 函數(shù)是否結(jié)束(result.done 屬性),如果沒結(jié)束,就將 next 函數(shù)再傳入 Thunk 函數(shù)(result.value 屬性),否則就直接退出。
有了這個執(zhí)行器,執(zhí)行 Generator 函數(shù)方便多了。不管有多少個異步操作,直接傳入 run 函數(shù)即可。當(dāng)然,前提是每一個異步操作,都要是 Thunk 函數(shù),也就是說,跟在 yield 命令后面的必須是 Thunk 函數(shù)。
var gen = function* (){var f1 = yield readFile('fileA');var f2 = yield readFile('fileB');// ...var fn = yield readFile('fileN'); };run(gen);上面代碼中,函數(shù) gen 封裝了 n 個異步的讀取文件操作,只要執(zhí)行 run 函數(shù),這些操作就會自動完成。這樣一來,異步操作不僅可以寫得像同步操作,而且一行代碼就可以執(zhí)行。
Thunk 函數(shù)并不是 Generator 函數(shù)自動執(zhí)行的唯一方案。因為自動執(zhí)行的關(guān)鍵是,必須有一種機制,自動控制 Generator 函數(shù)的流程,接收和交還程序的執(zhí)行權(quán)。回調(diào)函數(shù)可以做到這一點,Promise 對象也可以做到這一點。本系列的下一篇,將介紹基于 Promise 的自動執(zhí)行器。
總結(jié)
以上是生活随笔為你收集整理的Thunk 函数的含义和用法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Generator 函数的含义与用法
- 下一篇: React Router 使用教程