TOP1
前言
在上个章节中我们通过 Generator 体验了一种全新的异步流程书写方式, 我们在异步操作前面加上 yield 命名,通过promise对异步状态的绑定和指针就能控制程序是否交出执行权,同时也朝着”以同步方式书写异步代码”的目标,跨出了关键性的一步。
但是我们也发现了一些问题:
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
如果 Generator 函数中存在多个异步操作,我们必须通过next方法,手动的去改变内部指针。 这个过程会非常麻烦,所以我们要提供一种能自动执行机制,当异步操作有了结果,自动交回执行权。
两种方法可以做到这一点。
- Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。
- 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
第一种方式就如刚刚演示的代码,需要充斥着大量的回调跟then方法的调用。 接下来我们就来讲讲第二种方式将异步操作包装成 Thunk 函数。
TOP2
什么是thunk 函数?
Thunk函数早在上个世纪60年代就诞生了。
那时,编程语言刚刚起步,计算机学家还在研究,编译器怎么写比较好。一个争论的焦点是“求值策略”,即函数的参数到底应该何时求值。
var x = 1;
function f(m){
return m * 2;
}
f(x + 5)
上面代码先定义函数 f,然后向它传入表达式 x + 5 。请问,这个表达式应该何时求值?
一种意见是“传值调用”(call by value),即在进入函数体之前,就计算 x + 5 的值(等于6),再将这个值传入函数 f 。C语言就采用这种策略。
f(x + 5)
// 传值调用时,等同于
f(6)
另一种意见是“传名调用”(call by name),即直接将表达式 x + 5 传入函数体,只在用到它的时候求值。
f(x + 5)
// 传名调用时,等同于
(x + 5) * 2
传值调用和传名调用,哪一种比较好?回答是各有利弊。传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。
function f(a, b){
return b;
}
f(5 * 6 / 7 - 8, 9);
上面代码中,函数 f 的第一个参数是一个复杂的表达式,但是函数体内根本没用到。对这个参数求值,实际上是不必要的。
因此,有一些计算机学家倾向于”传名调用”,即只在执行时求值。
TOP3
Thunk 函数的含义
编译器的”传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。
var x = 6;
function f(n){
return n * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};
function f(thunk){
return thunk() * 2;
上面代码中,函数 f 的参数 x + 5 被一个函数替换了。凡是用到原参数的地方,对 Thunk 函数求值即可。
这就是 Thunk 函数的定义,它是”传名调用”的一种实现策略,用来替换某个表达式。
下面我们就基于 Thunk 函数写一个 Generator 执行器。
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
function run(gen){
var g = gen();
function next(data){
var result = g.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
}
next();
}
run(gen);
上面代码的run函数,就是一个 Generator 函数的自动执行器。内部的next函数就是 Thunk 的回调函数。next函数先将指针移到 Generator 函数头部的第一段的状态,然后判断 Generator 函数是否结束(result.done属性),如果没结束,就给第一段的异步操作返回的promise中通过then方法添加回调,并调用next方法,让指针从上一次停下来的地方开始执行,直到遇到下一个yield 表达式 或者return语句为止。
这样我们就能把手动的去改变内部指针,变成了自动执行。