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

相关文章
相关标签/搜索