JavaScript异步Thunk

在Generator一文中最后的例子,自动执行异步操作,还不够“自动”,毕竟每次调用异步API时,还需要手动指定resume回调函数以触发next。还有更“自动”的方式Thunk。感谢阮一峰提供的Thunk思路,可以参照这里。你可以从GitHub上获取本篇代码。

Thunk其实不是一个全新的概念,很早就有了,用于一个临时函数。什么意思呢?例如开发时会面临一个选择:什么时候开始求值?

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

对开发者来说这段代码非常正常,你不会多看它第二眼。问题是什么时候计算参数x + 5呢?

一种策略是立即计算,于是f(x + 5);会被转换成f(6);,然后进入函数f体内运行函数。

另一种策略是延迟计算,在进入函数f体之前并不计算参数值,而是进入函数体后才计算。因此函数体内return m * 2;会被转换成return (x + 5) * 2;,此时才开始计算。

如果不考虑开发成本,仅此例而言,延迟计算应该是比较好的。否则如果函数体内走了某if分支导致并没用到该参数,就白计算了,浪费性能。延迟到真正用到参数时才开始计算,这也是程序开发的一种流行的风格:越懒越好。

那如何实现延迟计算呢?可以生成一个临时函数(Thunk函数),将参数放进去里,上面代码等价于:

var x = 1;
var thunk = function () {   //Thunk函数
    return x + 5;
};
function f(tempFunc){
    return tempFunc() * 2;
};
f(thunk);   //12

上面这样的延迟计算,可能对效率控来说节省了一点理论上的性能(实际真节省了吗?未必),但从代码可读性,可维护性上来看,这样是得不偿失的。

再看看Thunk函数在JS里的应用,将多参的异步函数,转换成单参。通常异步函数的最后一个参数是回调函数。以NodeJS的核心模块File System的异步函数readFile为例

函数原型:fs.readFile(file[, options], callback)。支持3个参数,其中最后一个是回调函数。普通调用方式:

function someCallback(err, data) { 
    if (err) throw err;
    console.log(data); 
}
fs.readFile('./oranges.txt','utf8', someCallback);

用Thunk改造一下:

function someCallback(err, data) { 
    if (err) throw err;
    console.log(data); 
}
var Thunk = function (fileName, options){
    return function (callback){
        return fs.readFile(fileName, options, callback); 
    };
};

var readFileThunk = Thunk('./oranges.txt', 'utf8');
readFileThunk(someCallback);

看上去代码变复杂了。Thunk函数真正的作用是简化了参数,将原本多参的函数,简化成只接受回调函数做参数。即多参版本的异步函数,经由Thunk,变成了单参(参数为回调函数)函数。

现实中不必为每个异步函数定制一个Thunk函数,因此可以定义通用的Thunk函数:

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

var readFileThunk = Thunk(fs.readFile);
readFileThunk('./oranges.txt', 'utf8')(someCallback);

可以把上面Thunk函数放到common位置,任何多参的异步函数(最后一个参数为回调函数),都可以调用上面的Thunk函数转换成单参版本。

其实上述Thunk函数等价于柯里化

var readFileThunk = fs.readFile.bind(null, './oranges.txt', 'utf8');
readFileThunk(someCallback);

如果不想自己造轮子来写Thunk函数,可以安装Thunkify模块:npm install thunkify。源代码和我们写的Thunk非常像。

上面举的Thunk函数的例子,无论是延迟计算,还是将多参异步函数转换成单参,其实都没什么卵用。所以在Generator函数出现之前,Thunk函数确实没什么卵用。真正让其发挥作用的是配合Generator函数实现自动化异步操作。以读取文件为例,Generator函数封装了两个异步操作:

var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);

var gen = function* (){
    var r1 = yield readFileThunk('./apples.txt', 'utf8');
    console.log(r1);
    var r2 = yield readFileThunk('./oranges.txt', 'utf8');
    console.log(r2);
};

定义的异步操作很清晰(这也是Generator的优点,可以用同步化的方式定义异步操作步骤)。可以如下执行异步操作:

var g = gen();
var r = g.next();
r.value(function(err, data){    //r.value是一个function,等价于fs.readFile(callback)
    if (err) throw err;
    var r2 = g.next(data);
    r2.value(function(err, data){
        if (err) throw err;
        g.next(data);
    });
});

上面代码第二行执行next后,返回值r的value属性是Generator函数体内yield readFileThunk(‘./apples.txt’, ‘utf8’);语句的执行结果。即r的value属性是一个内部封装了[‘./apples.txt’, ‘utf8’]的单参数的fs.readFile函数。即r的value属性是fs.readFile(callback)函数。(再不明白,我也没有办法了…)

因此上面代码第三行r.value(function(err, data){…}等价于fs.readFile(function(err, data){…}。此时才开始正式执行异步函数读取文件内容。读取到的内容通过第5行next方法传递给Generator函数里的r1,打印出文件内容。之后就是重复上述套路。

显然开发者不想用这样嵌套的调用方法,太麻烦。所以参照Generator一文中例子的思路,可以定义一个run方法将上面的调用代码封装起来:

function run(genFunc) {
    var g = genFunc();
    function next(err, data) {
        var result = g.next(data);
        if (result.done) return;
        result.value(next);
    }
    next();
}
run(gen);

定义了run方法后,执行Generator函数就方便到令人发指。直接将Generator函数作为参数传给run就行了。然后会自动像多米诺骨牌一样依次执行Generator函数内的异步操作。当然,前提是每一个异步操作,都要是Thunk函数,即yield命令后面的必须是Thunk函数。

Thunk函数是自动执行Generator函数的一种选择,如果不习惯,或者觉得用Thunk函数并不会提高效率的话,可以像Generator一文中那样定义run,同样可以使Generator函数自动执行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容

  • 异步编程对JavaScript语言太重要。Javascript语言的执行环境是“单线程”的,如果没有异步编程,根本...
    呼呼哥阅读 7,302评论 5 22
  • 本文首发在个人博客:http://muyunyun.cn/posts/7b9fdc87/ 提到 Node.js, ...
    牧云云阅读 1,680评论 0 3
  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,708评论 0 5
  • 上一篇介绍了Promise异步编程,可以很好地回避回调地狱。但Promise的问题是,不管什么样的异步操作,被Pr...
    张歆琳阅读 1,434评论 0 13
  • 特别说明,为便于查阅,文章转自https://github.com/getify/You-Dont-Know-JS...
    杀破狼real阅读 483评论 0 0