前面讲到了promise 和generator
Promise提供了then链式写法,
generator提供了yield 状态机。
如果我们在yield 后返回一个promise会怎么样呢?
var fetch=require('node-fetch');
//异步任务的封装
//请求一个异步操作
function* generator(){
var url='https://api.github.com/users/github'
//yield 返回的value是一个fetch 也就是一个promise 对象
var result=yield fetch(url);
console.log(result.bio)
}
var g=generator();
//第一次next时返回的value是一个promise
var result=g.next();
console.log(result);
//第一个then可以获取yield 返回的数据,fetch返回的是页面状态。
//所以用json来获取返回的结果信息。并return
//第二个then获取json调用Next并传递json 这样generator内就可以获取到数据并进行下一个状态执行。
result.value.then(function(data){
console.log(data);
console.log('----------------------')
return data.json();
}).then(function(data){
console.log(data);
console.log('----------------------')
g.next(data);
})
//虽然generator 和promise结合可以使用,但是写法不是很简洁
参数的求值策略
那时,编程语言刚刚起步,计算机学家还在研究,编译器怎么写比较好。一个争论的焦点是"求值策略",即函数的参数到底应该何时求值。
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
传入函数体,只在用到它的时候求值。Haskell语言采用这种策略。
f(x + 5) // 传名调用时,等同于 (x + 5) * 2
传值调用和传名调用,哪一种比较好?回答是各有利弊。传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。
编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做Thunk函数。
function f(m){
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};
function f(thunk){
return thunk() * 2;
}
上面代码中,函数f的参数x + 5
被一个函数替换了。凡是用到原参数的地方,对Thunk
函数求值即可。 这就是Thunk函数的定义,它是"传名调用"的一种实现策略,用来替换某个表达式。
JavaScript语言是传值调用,它的Thunk函数含义有所不同。在JavaScript语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。
// 正常版本的readFile(多参数版本) fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本) var readFileThunk = Thunk(fileName);
readFileThunk(callback);
var Thunk = function (fileName){
return function (callback){
return fs.readFile(fileName, callback);
};
};
上面代码中,fs模块的readFile方法是一个多参数函数,两个参数分别为文件名和回调函数。经过转换器处理,它变成了一个单参数函数,只接受回调函数作为参数。这个单参数版本,就叫做Thunk函数。
可以将方法转换成thunk模式
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
// ... });
你可能会问, Thunk函数有什么用?回答是以前确实没什么用,但是ES6有了Generator函数,Thunk函数现在可以用于Generator函数的自动流程管理。
以读取文件为例。下面的Generator函数封装了两个异步操作。
var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);
var gen = function* (){
var r1 = yield readFile('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFile('/etc/shells');
console.log(r2.toString());
};
上面代码中,yield命令用于将程序的执行权移出Generator函数,那么就需要一种方法,将执行权再交还给Generator函数。
这种方法就是Thunk函数,因为它可以在回调函数里,将执行权交还给Generator函数。为了便于理解,我们先看如何手动执行上面这个Generator函数。
var g = gen();
var r1 = g.next();
r1.value(function(err, data){
if (err) throw err;
var r2 = g.next(data);
r2.value(function(err, data){
if (err) throw err;
g.next(data);
});
});
上面代码中,变量g是Generator函数的内部指针,表示目前执行到哪一步。next方法负责将指针移动到下一步,并返回该步的信息(value属性和done属性)。
仔细查看上面的代码,可以发现Generator函数的执行过程,其实是将同一个回调函数,反复传入next方法的value属性。这使得我们可以用递归来自动完成这个过程。
Thunk函数真正的威力,在于可以自动执行Generator函数。下面就是一个基于Thunk函数的Generator执行器。
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
run(gen);
//有了这个run方法后就不必每个next都进行判断了。
co模块是著名程序员TJ Holowaychuk于2013年6月发布的一个小工具,用于Generator函数的自动执行。
比如,有一个Generator函数,用于依次读取两个文件。
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
co模块可以让你不用编写Generator函数的执行器。
var co = require('co');
co(gen);
上面代码中,Generator函数只要传入co函数,就会自动执行。
co函数返回一个Promise对象,因此可以用then方法添加回调函数。
co(gen).then(function (){
console.log('Generator 函数执行完成');
})
上面代码中,等到Generator函数执行结束,就会输出一行提示。
/*还是沿用上面的例子。首先,把fs模块的readFile方法包装成一个Promise对象。
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
然后,手动执行上面的Generator函数。
var g = gen();
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data);
});
})
手动执行其实就是用then方法,层层添加回调函数。理解了这一点,就可以写出一个自动执行器。
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);
上面代码中,只要Generator函数还没执行到最后一步,next函数就调用自身,以此实现自动执行。
ES7提供了async
函数,使得异步操作变得更加方便。async
函数是什么?一句话,async
函数就是Generator函数的语法糖。
var asyncReadFile = async function (){
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
await 实现了我们对promise的封装