现状

目前我们对异步回调的解决方案有这么几种:回调,deferred/promise和事件触发。回调的方式自不必说,需要硬编码调用,而且有可能会出现复杂的嵌套关系,造成“回调黑洞”;deferred/promise方式则对使用者而言简洁明了,在执行异步函数之前就已经构造好了执行链--then链,而且实现也很灵活,具体可参考Promise的实现;事件机制则是一种观察者模式的实现,但也必须硬编码在异步执行的函数中,当异步函数执行完毕后再trigger相关事件,而观察者则相应执行事件处理函数。

注意,刚刚提到了一个词--硬编码,依赖这种方式仅实现回调局限性很大,如在node中,对fs.readFile('file1','utf-8')完成之后再进行fs.readFile('file2','utf-8'),使用回调和事件触发则必须在第一个异步的回调函数中进行调用trigger,增强了这两个操作的强依赖,使用deferred/promise则会很好的避免。

现在,随着ECMAScript6的逐渐普及,我们可以在chrome和node端尝试一种新的异步流程控制--generator。通过generator,我们可以控制函数内部的执行阶段,进而可以利用高阶函数的特性进行扩展,完成对异步流程的控制。

特性及兼容性

由于隶属于ECMAScript6规范,因此兼容性是一个大问题,不过我们在最新版的chrome和node --harmony下使用该功能,所以做node端开发的小伙伴们可以大胆的使用。

那么,什么是generator呢?

function* (){}

这就是一个匿名的generator。通过function关键字和函数名或者括号之间添加“*”定义一个generator函数,我们也可以这样判断一个函数是否为generator:

typeof fn == 'function' && fn.constructor.name == 'GeneratorFunction'

在generator中我们可以关键字yield,java程序员对yield肯定不陌生,yield在java中是线程调度的一种方式,可以释放时间片让同级别的线程执行,然而在js中,yield却大不相同,因为js的执行线程是单线程,所以调度就不存在,yield我们可以理解为函数执行的一个断点,每次只能执行到yield处,这样原本顺序或者异步执行的函数逻辑都可以通过某种方式使他们以顺序的方式呈现在我们眼前,在这里需要强调下,通过yield只能断点执行generator函数中的逻辑,在函数之外并不会阻塞,否则整个主线程就会挂掉

一个generator函数执行到yield处,我们通过调用generator object的next()继续进行,generator object(下文简写为GO)就是generator函数的返回对象,调用GO的next方法会返回一个{value: '',done: false}这样的对象,value为yield关键字后面的表达式的值,done则表示generator函数是否执行完毕。

这就是基本的generator所有的数据结构,很简单明了。

实例

function * fn(){
var a = yield 1;
console.log(a);
var b = yield 2;
console.log(b);
}
var go = fn(); // 这是一个generator object
go.next(); // 执行到第一个 yield ,执行表达式 1
go.next(); //执行到第二个yield,输出console.log(a)为undefined,执行表达式 2
go.next(); //执行console.log(b),输出 undefined

上面的demo很容易理解,可能唯一有点疑问的就是console.log的输出。这里强调,每次next,只执行yield后面的表达式,这样对于前面的赋值操作就无能为力,那么如何对a进行赋值呢?可以通过第二个next进行传值。通过对第二个go.next(2),这样a的值就被赋为2,同理b的值也可以这样传递。

但是,这对于异步流程控制有什么用呢?其实,还是通过分段执行异步操作来完成。每个yield async1()执行完毕,将结果作为参数传给下一个yield async2(),这样我们只需判断GO.done是否为true来终止这个流程。

异步流程控制

我们的目标是实现这种方式的流程控制:

flow(function *(){
var readFile = helper(fs.readFile);
var t1 = yield readFile('./files/f1', 'utf8');
var t2 = yield readFile(t1, 'utf8');
console.log(t2);
});

其中flow是流程控制函数,参数为一个generator,helper函数则是一个包装函数,负责针对异步操作进行处理,下面我们看看helper函数的逻辑。

var helper = function(fn) {
var feed; // 用于存储回调函数,该函数复用于所有用于helper处理的异步函数
/**
* 执行次序分析:
* helper的参数fn是一个异步函数,通过helper的处理,返回一个含有内部处理逻辑
* 的函数,该函数封装了所需参数和可能的回调函数feed,并且返回一个设置feed的函数。
*
* 在具体的使用中,通过helper函数封装fs.readFile,获取readFile。当执行第一个
* 片段时,首先将所有的参数(包括feed)合并到args,并执行异步调用返回处理函数;此时
* 我们用获取的返回函数设置回调函数,进而影响到args中的最后一项的函数
*/ return function(){
var args = [].slice.call(arguments); args.push(function(){
if(feed) {
feed.apply(null,arguments);
}
console.log(feed)
});
fn.apply(null,args);
// 返回一个函数,用于给yield之前的变量赋值
return function(fn){
feed = fn;
}
};

helper函数的作用就是重新包装异步函数,返回的包装函数也会返回一个函数,用于给回调函数feed赋值。

所有的异步函数都需要用helper进行封装,已传递必要的回调,最后按照flow分发的流程“依次执行”。

下面我们实现flow的控制逻辑:

var flow = function(gfn) {
var generator = gfn();
next(); function next(data){
generator.ret = generator.next(data);
if(generator.ret.done){
return;
}
generator.ret.value(
function(error,d){
if(error)
throw error; next.apply(null,[].slice.call(arguments,1));
}
);
}
};

逻辑依旧很简单,针对传入的generator生产generator object,最后进入next递归。在递归中,首先执行next逻辑并判断是否到了generator的终点,如果没有则调用generator object的value方法(此处为“被helper处理过得函数的返回值,即function(fn){feed = fn}”)对回调进行赋值,在回调中则递归执行next函数,直至generator结束逻辑。

通过这样的方式,我们制定了flow流程,可以将多个异步操作顺序执行,而不影响generator函数之外的其余逻辑,这样避免了硬编码,没有了回调黑洞,我们只需在异步函数前加yield即可,省时省事。

flow(function *(){
var readFile = helper(fs.readFile);
var nt = helper(process.nextTick);
var t1 = yield readFile('./files/f1', 'utf8');
var t2 = yield readFile(t1, 'utf8');
yield nt(function(){console.log(t2)});
// console.log(t2);
});

可以用helper封装各种异步回调,在具体的业务逻辑中传入其余回调返回值作为参数,从而达到目的。

并行异步执行

yield 后面不仅仅可以放置表达式,也可以放置数组。数组的每项为表达式,这样每次执行到yield时,会并行执行这些异步操作,返回对象的value属性也是一个数组,我们依旧可以对value数组的每项进行赋值,从而完成回调的赋值。

var length = generator.ret.value.length,
ret = [];
generator.ret.value.forEach(function(item,i){ item(function(err,data) {
--length;
if (err) {
console.log(err.message);
// throw err;
}
ret.push(data);
if(0 == length){
generator.next(ret);
}
}); });

对value值进行遍历,并判断并行的异步操作是否都已完成,若完成则传递ret数组给变量。

throw特性

这块throw语法糖是后来添加的,之所以提到它是因为它的表现有点独特:

var gen = function* gen() {
try {
yield console.log('hello');
yield console.log('world');
}
catch (e) {
console.log(e);
yield console.log('error...');
}
yield console.log('end');
} var g = gen(); g.next();
g.throw('a');
g.next();

第一个next后,输出‘hello’;

throw后,输出‘a’、‘error...’

第二个next后,输出‘end’

可以发现gen.throw后,不仅执行到catch代码块,而且还会执行下一个yield表达式,在这里需要注意下!

应用

目前generator的兼容性要求其只能在node平台上使用,目前express框架的后继者koa采用了generator实现中间件的方式,中间件处理完每个请求都会通过yield *next的方式进行分发,此处的next也是一个generator object,通过yield *next的方式可以嵌套多层generator链,这样next()就会到下一个generator的yield处。

分解函数的执行,这种方式确实让人耳目一新,我们有理由相信js的未来,我们要坚信js未来的能量,我们要自豪我们处在前端开发这个领域内。

使用yield进行异步流程控制的更多相关文章

  1. node核心:异步流程控制

    Node.js的异步是整个学习Node.js过程中重中之重. 1)异步流程控制学习重点 2)Api写法:Error-first Callback 和 EventEmitter 3)中流砥柱:Promi ...

  2. js 异步流程控制之 avQ(avril.queue)

    废话前言 写了多年的js,遇到过最蛋疼的事情莫过于callback hell, 相信大家也感同身受. 业界许多大大也为此提出了很多不错的解决方案,我所了解的主要有: 朴灵 event proxy, 简 ...

  3. 异步流程控制库GoWithTheFlow

    异步流程控制库GoWithTheFlow 一个尾触发方式来控制异步流程的库, 有seq(顺序执行) par(同步执行) 两种方法 博客 http://notes.jetienne.com/2011/0 ...

  4. 【javascript】Promise/A+ 规范简单实现 异步流程控制思想

    ——基于es6:Promise/A+ 规范简单实现 异步流程控制思想  前言: nodejs强大的异步处理能力使得它在服务器端大放异彩,基于它的应用不断的增加,但是异步随之带来的嵌套.难以理解的代码让 ...

  5. Nodejs中使用异步流程控制Async

    首先,我们都知道,Node基于事件驱动的异步I/O架构,所谓异步就是非阻塞,说白了就是一个事件执行了,我不必等待它执行完成后我才能执行下一个事件.所以在Node环境中的模块基本都是异步的,上一篇说到我 ...

  6. nodejs进阶(7)—async异步流程控制

    Async介绍 Async是一个流程控制工具包,提供了直接而强大的异步功能.基于Javascript为Node.js设计,同时也可以直接在浏览器中使用. Async提供了大约20个函数,包括常用的 m ...

  7. node基础13:异步流程控制

    1.流程控制 因为在node中大部分的api都是异步的,比如说读取文件,如果采用回调函数的形式,很容易造成地狱回调,代码非常不容易进行维护. 因此,为了解决这个问题,有大神写了async这个中间件.极 ...

  8. (一)Nodejs - 框架类库 - Nodejs异步流程控制Async

    简介 Async是一个流程控制工具包,提供了直接而强大的异步功能 应用场景 业务流程逻辑复杂,适应异步编程,减少回调的嵌套 安装 npm insatll async 函数介绍 Collections ...

  9. async 异步流程控制规则

    github 学习async网址 : https://github.com/caolan/async/ 1.Async 函数介绍 async 主要实现了三个部分的流程控制功能 1.集合:Collect ...

随机推荐

  1. C++ 小知识积累

    (1)setw和setfill函数 #include<iomanip> 代码: #include<iostream> #include<iomanip> using ...

  2. JS的面向对象编程一:封装

    Javascript是一种基于对象的语言,但它又没有class.这在很长的一段时间里,对JS的面向对象编程的概念很模糊,在编程的时候时有用到,但要说个所以然,却说不出来,所以看了些书,又在网上查了些资 ...

  3. 天气预报API开发

    天气预报API开发 一.        寻觅篇 最近想要跟着视频练习一下利用API开发一个天气预报系统,就在网上找了一下可以用的API,结果好多都已经失效了... 1.       百度车联网天气预报 ...

  4. 协议分析 - DHCP协议解码详解

    协议分析 - DHCP协议解码详解 [DHCP协议简介]         DHCP,全称是 Dynamic Host Configuration Protocol﹐中文名为动态主机配置协议,它的前身是 ...

  5. SQL Server 变更数据捕获(CDC)监控表数据

    一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents) 背景(Contexts) 实现过程(Realization) 补充说明(Addon) 参考文献(References) ...

  6. Excel导入导出组件的设计

    前言: 距离一篇文章,又八九个月过去了,随着在园子露脸的次数越来越少,正如我们淡忘上一波大神那样,我们也正下一波所淡忘. 这八九个月,前前后,游走在十来个项目上,忙,却找不到成就感. 人过30后,也是 ...

  7. AngularJS基础入门初探

    一.AngularJS简介 1.1 什么是AngularJS (1)一款非常优秀的前端JS框架,可以方便实现MVC/MVVM模式 (2)由Misko Hevery 等人创建,2009年被Google所 ...

  8. Python yield与实现

    Python yield与实现  yield的功能类似于return,但是不同之处在于它返回的是生成器. 生成器 生成器是通过一个或多个yield表达式构成的函数,每一个生成器都是一个迭代器(但是迭 ...

  9. ASP.NET MVC学前篇之请求流程

    ASP.NET MVC学前篇之请求流程 请求流程描述 对于请求的流程,文章的重点是讲HttpApplication和HttpModule之间的关系,以及一个简单的示例实现.(HttpModule又是M ...

  10. [ASP.NET MVC 小牛之路]18 - Web API

    Web API 是ASP.NET平台新加的一个特性,它可以简单快速地创建Web服务为HTTP客户端提供API.Web API 使用的基础库是和一般的MVC框架一样的,但Web API并不是MVC框架的 ...