Thunk深刻解析

一步步打造thunkify

本文的思路:javascript

  1. 學習thunk相關知識,主要參考阮一峯的介紹html

  2. 一步步實現thunkify模塊,而且使用測試用例來完善咱們的代碼,打造出一個健壯的模塊java

1. 誕生背景

Thunk函數的誕生是源於一個編譯器設計的問題:求值策略,即函數的參數到底應該什麼時候求值。node

例如:git

var x = 1;
function f(m) {
    return m * 2;
}
f(x + 5);

其中x+5這個表達式應該何時求值,有兩種思路github

  • 傳值調用(call by value),即在進入函數體之間,先計算x+5的值,再將這個值(6)傳入函數f,例如c語言,這種作法的好處是實現比較簡單,可是有可能會形成性能損失。app

  • 傳名調用(call by name),即直接將表達式(x+5)傳入函數體,只在用到它的時候求值。函數

2. Thunk函數的含義

編譯器的傳名調用實現,每每就是將參數放到一個臨時函數之中,再將這個臨時函數轉入函數體,這個臨時函數就叫作Thunk函數性能

來看一段代碼示例:學習

function f(m) {
    return m*2;
}

f(x + 5);

// 等價於如下代碼
var thunk = function () {
    return x + 5;
};

function f(thunk) {
    return thunk() * 2;
}

3. javascript中的Thunk函數

咱們都知道Javascript是傳值調用的,那麼js中的Thunk函數又是怎麼回事?

在Javascript語言中,Thunk函數替換的不是表達式,而是多參數函數,將其替換成單參數的版本,且只接受回調函數做爲參數。

仍是經過代碼來理解,即

// 正常版本的readFile,須要兩個參數filename、callback
fs.readFile(fileName, callback);

// thunk版本的readFile
var readFileThunk = thunkify(fs.readFile);
readFileThunk(fileName)(callback);

原文中例子就是柯里化,預置參數fileName,直接調用fs.readFile

好,如今咱們來思考如何實現thunkify函數。咱們從調用的形式來看,返回的應該是一個高階函數,即返回一個函數a,a的返回仍是一個函數。

var thunkify = function (fn) {
    return function () {
        return function () {

        }
    }
};

結合上述例子,由於是包裝函數,所以最終仍是readFile執行,且須要fileName,所以:

var thunkify = function (fn) {
    return function () {
        var args = Array.prototype.slice.call(arguments);
        return function (callback) {
            args.push(callback);
            return fn.apply(this, args);
        }
    }
};

這樣彷佛很完美,咱們運行整個示例

const fs = require('fs');

var thunkify = function (fn) {
    return function () {
        var args = Array.prototype.slice.call(arguments);
        return function (callback) {
            args.push(callback);
            return fn.apply(this, args);
        }
    }
};

var readFileThunk = thunkify(fs.readFile);
readFileThunk('test.txt', 'utf-8')( (err, data) => {
    console.log(data);
});

運行結果爲

圖片描述

4. 打造thunkify模塊

要寫出一個健壯的thunkify函數,須要考慮的各類狀況,而咱們經過tj大神寫的thunkify模塊的測試代碼,來看看咱們本身的thunkify還存在哪些不足,一步步來優化。

一、保存上下文的問題

function load(fn) {
  fn(null, this.name);
}

var user = {
  name: 'tobi',
  load: thunkify(load)
};

user.load()((err, res) => {
    console.log(res);
});

運行以後,res的結果爲undefined,緣由是沒有保存上下文,改進一下

var thunkify = function (fn) {
    return function () {
        var args = Array.prototype.slice.call(arguments);
        var ctx = this;
        return function (callback) {
            args.push(callback);
            return fn.apply(ctx, args);
        }
    }
};

二、捕抓錯誤

function load(fn) {
  throw new Error('boom');
}
load = thunkify(load);
load()(err => console.log(err.message));

運行以後,發現並無捕抓到錯誤,咱們須要執行函數進行try/catch,而且當出錯時,傳遞出錯信息。

var thunkify = function (fn) {
    return function () {
        var args = Array.prototype.slice.call(arguments);
        var ctx = this;
        return function (callback) {
            args.push(callback);
            var result;
            // try/catch捕抓信息,而且出錯時,傳遞給回調函數
            try {
                result = fn.apply(ctx, args);
            } catch (e) {
                callback(e);
            }
            return result;
        }
    }
};

三、回調函數應該只調用一次。

function load(fn) {
  fn(null, 1);
  fn(null, 2);
  fn(null, 3);
}

load = thunkify(load);

load()((err,ret) => console.log(ret));

運行輸出結果爲1 2 3,而咱們指望結果只爲1,那麼須要判斷callback是否已經執行過了,使其只執行一次。

var thunkify = function (fn) {
    return function () {
        var args = Array.prototype.slice.call(arguments);
        var ctx = this;
        return function (callback) {
            var called; 
            // 對callback進行封裝,使其只能執行一次。
            args.push(function () {
                if(called) return;
                called = true;
                callback.apply(null, arguments);
            });
            var result;
            try {
                result = fn.apply(ctx, args);
            } catch (e) {
                callback(e);
            }
            return result;
        }
    }
};

到這裏,咱們經過了全部的測試,完成了一個健壯thunkify模塊。

5. 總結

在學習一個概念或者一個模塊時,測試代碼加深你對知識的理解和掌握。

來源

  1. Thunk-阮一峯

  2. thunkify-tj

相關文章
相關標籤/搜索