Thunk函式的使用

WindrunnerMax發表於2020-06-19

Thunk函式的使用

編譯器的求值策略通常分為傳值呼叫以及傳名呼叫,Thunk函式是應用於編譯器的傳名呼叫實現,往往是將引數放到一個臨時函式之中,再將這個臨時函式傳入函式體,這個臨時函式就叫做Thunk 函式。

求值策略

編譯器的求值策略通常分為傳值呼叫以及傳名呼叫,在下面的例子中,將一個表示式作為引數進行傳遞,傳值呼叫以及傳名呼叫中實現的方式有所不同。

var x = 1;

function s(y){
    console.log(y + 1); // 3
}

s(x + 1);

在上述的例子中,無論是使用傳值呼叫還是使用傳名呼叫,執行的結果都是一樣的,但是其呼叫過程不同:

  • 傳值呼叫:首先計算x + 1,然後將計算結果2傳遞到s函式,即相當於呼叫s(2)
  • 傳名呼叫:直接將x + 1表示式傳遞給y,使用時再計算x + 1,即相當於計算(x + 1) + 1

傳值呼叫與傳名呼叫各有利弊,傳值呼叫比較簡單,但是對引數求值的時候,實際上還沒用到這個引數,有可能造成沒有必要的計算。傳名呼叫可以解決這個問題,但是實現相對來說比較複雜。

var x = 1;

function s(y){
    console.log(y + 1); // 3
}

s(x + 1, x + 2);

在上面這個例子中,函式s並沒有用到x + 2這個表示式求得的值,使用傳名呼叫的話只將表示式傳入而並未計算,只要在函式中沒有用到x + 2這個表示式就不會計算,使用傳值呼叫的話就會首先將x + 2的值計算然後傳入,如果沒有用到這個值,那麼就多了一次沒有必要的計算。Thunk函式就是作為傳名呼叫的實現而構建的,往往是將引數放到一個臨時函式之中,再將這個臨時函式傳入函式體,這個臨時函式就叫做Thunk 函式。

var x = 1;

function s(y){
    console.log(y + 1); // 3
}

s(x + 1);

// 等同於

var x = 1;

function s(thunk){
    console.log(thunk() + 1); // 3
}

var thunk = function(){
    return x + 1;
}

s(thunk);

Js中的Thunk函式

Js中的求值策略是是傳值呼叫,在Js中使用Thunk函式需要手動進行實現且含義有所不同,在Js中,Thunk函式替換的不是表示式,而是多引數函式,將其替換成單引數的版本,且只接受回撥函式作為引數。

// 假設一個延時函式需要傳遞一些引數
// 通常使用的版本如下
var delayAsync = function(time, callback, ...args){
    setTimeout(() => callback(...args), time);
}

var callback = function(x, y, z){
    console.log(x, y, z);
}

delayAsync(1000, callback, 1, 2, 3);

// 使用Thunk函式

var thunk = function(time, ...args){
    return function(callback){
        setTimeout(() => callback(...args), time);
    }
}

var callback = function(x, y, z){
    console.log(x, y, z);
}

var delayAsyncThunk = thunk(1000, 1, 2, 3);
delayAsyncThunk(callback);

實現一個簡單的Thunk函式轉換器,對於任何函式,只要引數有回撥函式,就能寫成Thunk函式的形式。

var convertToThunk = function(funct){
  return function (...args){
    return function (callback){
      return funct.apply(this, args);
    }
  };
};

var callback = function(x, y, z){
    console.log(x, y, z);
}

var delayAsyncThunk = convertToThunk(function(time, ...args){
    setTimeout(() => callback(...args), time);
});

thunkFunct = delayAsyncThunk(1000, 1, 2, 3);
thunkFunct(callback);

Thunk函式在ES6之前可能應用比較少,但是在ES6之後,出現了Generator函式,通過使用Thunk函式就可以可以用於Generator函式的自動流程管理。首先是關於Generator函式的基本使用,呼叫一個生成器函式並不會馬上執行它裡面的語句,而是返回一個這個生成器的迭代器iterator 物件,他是一個指向內部狀態物件的指標。當這個迭代器的next()方法被首次(後續)呼叫時,其內的語句會執行到第一個(後續)出現yield的位置為止,yield後緊跟迭代器要返回的值,也就是指標就會從函式頭部或者上一次停下來的地方開始執行到下一個yield。或者如果用的是yield*,則表示將執行權移交給另一個生成器函式(當前生成器暫停執行)。

function* f(x) {
    yield x + 10;
    yield x + 20;
    return x + 30;
}
var g = f(1);
console.log(g); // f {<suspended>}
console.log(g.next()); // {value: 11, done: false}
console.log(g.next()); // {value: 21, done: false}
console.log(g.next()); // {value: 31, done: true}
console.log(g.next()); // {value: undefined, done: true} // 可以無限next(),但是value總為undefined,done總為true

由於Generator函式能夠將函式的執行暫時掛起,那麼他就完全可以操作一個非同步任務,當上一個任務完成之後再繼續下一個任務,下面這個例子就是將一個非同步任務同步化表達,當上一個延時定時器完成之後才會進行下一個定時器任務,可以通過這種方式解決一個非同步巢狀的問題,例如利用回撥的方式需要在一個網路請求之後加入一次回撥進行下一次請求,很容易造成回撥地獄,而通過Generator函式就可以解決這個問題,事實上async/await就是利用的Generator函式以及Promise實現的非同步解決方案。

var it = null;

function f(){
    var rand = Math.random() * 2;
    setTimeout(function(){
        if(it) it.next(rand);
    },1000)
}

function* g(){ 
    var r1 = yield f();
    console.log(r1);
    var r2 = yield f();
    console.log(r2);
    var r3 = yield f();
    console.log(r3);
}

it = g();
it.next();

雖然上邊的例子能夠自動執行,但是不夠方便,現在實現一個Thunk函式的自動流程管理,其自動幫我們進行回撥函式的處理,只需要在Thunk函式中傳遞一些函式執行所需要的引數比如例子中的index,然後就可以編寫Generator函式的函式體,通過左邊的變數接收Thunk函式中funct執行的引數,在使用Thunk函式進行自動流程管理時,必須保證yield後是一個Thunk函式。
關於自動流程管理run函式,首先需要知道在呼叫next()方法時,如果傳入了引數,那麼這個引數會傳給上一條執行的yield語句左邊的變數,在這個函式中,第一次執行next時並未傳遞引數,而且在第一個yield上邊也並不存在接收變數的語句,無需傳遞引數,接下來就是判斷是否執行完這個生成器函式,在這裡並沒有執行完,那麼將自定義的next函式傳入res.value中,這裡需要注意res.value是一個函式,可以在下邊的例子中將註釋的那一行執行,然後就可以看到這個值是f(funct){...},此時我們將自定義的next函式傳遞後,就將next的執行許可權交予了f這個函式,在這個函式執行完非同步任務後,會執行回撥函式,在這個回撥函式中會觸發生成器的下一個next方法,並且這個next方法是傳遞了引數的,上文提到傳入引數後會將其傳遞給上一條執行的yield語句左邊的變數,那麼在這一次執行中會將這個引數值傳遞給r1,然後在繼續執行next,不斷往復,直到生成器函式結束執行,這樣就實現了流程的自動管理。

function thunkFunct(index){
    return function f(funct){
        var rand = Math.random() * 2;
        setTimeout(() => funct({rand:rand, index: index}), 1000)
    }
}

function* g(){ 
    var r1 = yield thunkFunct(1);
    console.log(r1.index, r1.rand);
    var r2 = yield thunkFunct(2);
    console.log(r2.index, r2.rand);
    var r3 = yield thunkFunct(3);
    console.log(r3.index, r3.rand);
}

function run(generator){
    var g = generator();

    var next = function(data){
        var res = g.next(data);
        if(res.done) return ;
        // console.log(res.value);
        res.value(next);
    }

    next();
}

run(g);

每日一題

https://github.com/WindrunnerMax/EveryDay

參考

https://www.jianshu.com/p/9302a1d01113
https://segmentfault.com/a/1190000017211798
http://www.ruanyifeng.com/blog/2015/05/thunk.html

相關文章