ECMAScrpt6 异步最终解决方案

来源:互联网 发布:淘宝网1元秒杀在哪里找 编辑:程序博客网 时间:2024/05/22 12:23

前面讲到了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结合可以使用,但是写法不是很简洁

fetch用来返回一个promise对象
g.next()返回的是一个promise,
所以可以用value.then来获取状态

Thunk函数


参数的求值策略

那时,编程语言刚刚起步,计算机学家还在研究,编译器怎么写比较好。一个争论的焦点是"求值策略",即函数的参数到底应该何时求值。

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函数的含义

编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做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函数含义有所不同。在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函数。


thunkify模块

可以将方法转换成thunk模式

var thunkify = require('thunkify');var fs = require('fs');var read = thunkify(fs.readFile);read('package.json')(function(err, str){  // ...});

Generator 函数的流程管理

你可能会问, 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函数的自动流程管理

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都进行判断了。
//可以一次执行完所有的next
//但是要求 yield 后表达式必需要thunk函数。

/*
Thunk函数并不是Generator函数自动执行的唯一方案。
因为自动执行的关键是,必须有一种机制,
自动控制Generator函数的流程,接收和交还程序的执行权。
回调函数可以做到这一点,Promise 对象也可以做到这一点。
*/

co模块

基本用法

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函数执行结束,就会输出一行提示。

/*
前面说过,Generator就是一个异步操作的容器。它的自动执行需要一种机制,
当异步操作有了结果,能够自动交回执行权。
两种方法可以做到这一点。
(1)回调函数。将异步操作包装成Thunk函数,在回调函数里面交回执行权。
(2)Promise 对象。将异步操作包装成Promise对象,用then方法交回执行权。


co模块其实就是将两种自动执行器(Thunk函数和Promise对象),
包装成一个模块。使用co的前提条件是,
Generator函数的yield命令后面,只能是Thunk函数或Promise对象。
*/

基于Promise对象的自动执行

还是沿用上面的例子。首先,把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函数就调用自身,以此实现自动执行。

async函数

含义

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的封装

async 函数的用法

同Generator函数一样,async函数返回一个Promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

Async函数有多种使用形式。

// 函数声明async function foo() {}// 函数表达式const foo = async function () {};// 对象的方法let obj = { async foo() {} }// 箭头函数const foo = async () => {};

注意点

第一点,await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。

第二点,多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

let foo = await getFoo();let bar = await getBar();

上面代码中,getFoogetBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。

// 写法一let [foo, bar] = await Promise.all([getFoo(), getBar()]);// 写法二let fooPromise = getFoo();let barPromise = getBar();let foo = await fooPromise;let bar = await barPromise;

上面两种写法,getFoogetBar都是同时触发,这样就会缩短程序的执行时间。


0 0
原创粉丝点击