ECMAScrpt6 异步最终解决方案

前面讲到了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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值