背景

在JS的使用场景中,异步操作的处理是一个不可回避的问题,如果不做任何抽象、组织,只是“跟着感觉走”,那么面对“按顺序发起3个ajax请求”的需求,很容易就能写出如下代码(假设已引入jQuery):

// 第1个ajax请求
$.ajax({
url:'http://echo.113.im',
dateType:'json',
type:'get',
data:{
data:JSON.stringify({status:1,data:'hello world'}),
type:'json',
timeout:1000
},
success:function(data){
if(data.status === 1){
// 第2个ajax请求
$.ajax({
......此处省略500字
success:function(data){
if(data.status === 1){
// 第3个ajax请求
$.ajax({
......此处省略500字
success:function(data){
if(data.status === 1){
 
}
}
});
}
}
});
}
}
});

当顺序执行的异步操作越来越多的时候,回调层级也就越多,这也就是传说中的“回调恶魔金字塔”。

生成器的卢山真面目

所谓“生成器”,其实是一个函数,但是这个函数的行为会比较特殊:

  1. 它并不直接执行逻辑,而是用来生成另一个对象(这也正是“生成器”的含义)
  2. 它所生成的对象中的函数可以把逻辑拆开来,一片一片调用执行,而不是像普通的函数,只能从头到尾一次执行完毕

生成器的语法和普通函数类似,特殊之处在于:

  1. 字面量(函数声明/函数表达式)的关键字function后面多了一个*,而且这个*前后允许有空白字符
  2. 函数体中多了yield运算符

举个粟子:

function * GenA(){
console.log('from GenA, first.');
yield 1;
console.log('from GenA, second.');
var value3 = yield 2;
console.log('from GenA, third.',value3);
return 3;
}
 
var a = GenA();

接下来依次执行:

a.next();
// from GenA, first.
// Object {value:1,done:false}
 
a.next();
// from GenA, second.
// Object {value:2,done:false}
 
a.next(333);
// from GenA, third.
// 333
// Object {value:3,done:true}
 
a.next();
// Object {value:undefined,done:true}

这个例子反映了生成器的基本用法,有以下几点值得注意:

  1. 在调用GenA()时,函数体中的逻辑并不会执行(控制台没有输出),直接调用a.next()时才会执行
  2. a是一个对象,它由生成器GenA()调用而来,注意GenA()并没有返回a对象,这非常像构造函数的执行形式,但是不允许添加new
  3. 调用a.next()时,函数体中的逻辑才开始真正执行,每次调用时会到yield语句结束,并将yield的运算数作为结果返回
  4. a.next()返回的结果是一个对象,对yield的运算数做了包装,并带上了done属性
  5. done属性为false时,表示该函数逻辑还未执行完,可以调用a.next()继续执行
  6. 最后一次返回的结果为return语句返回的结果,且done值为true。如果不写return,则值为undefined
  7. value3 = yield 2这句是指,这一段逻辑返回2,在下一次调用a.next()时,将参数赋给value3。换句话说,这句只执行了后面半段就暂停了,等到再次调用a.next()时才会将参数赋给value3并继续执行下面的逻辑
  8. 返回值中donetrue时,仍然可以继续调用,返回的值为undefined

同步场景下生成器的使用

来看看同步场景下,如何使用生成器:

function * Square(){
for(var i=1;;i++){
yield i*i;
}
}
 
var square = Square();
 
square.next(); // 1
square.next(); // 4
square.next(); // 9
......

同步场景下大概就是这么用的,很无趣是吧?我也这么觉得,其实和直接函数调用差别不大。不过值得注意的是,我们在循环中并没有设中止条件,因为调用一个square.next()方法,它才会执行一次,不调用则不执行,所以不用担心死循环的问题。

异步场景下的生成器使用

如何用生成器解决异步场景下的“回调恶魔金字塔”呢?满心期待对吧,很遗憾,它并不能那么简单地解决……

从前面的例子中,其实已经可以体会出来了,生成器的用法中并不包含对异步的处理,所以其实没有办法帮助我们对异步回调进行封闭。那么为什么大家将它视为解决回调嵌套的神器呢?在翻阅了不少资料后找到这篇文章,文章作者一开始也认为生成器并不能解决回调嵌套的问题,但下面自己做了解释,如果生成器的返回的是一系列的Promise对象的话,情况就会不一样了,举个粟子:

function myAjax(){
return fetch('http://echo.113.im?data=1');
}

我们使用window.fetch方法来处理ajax请求,这个方法会返回一个Promise对象。然后,我们使用一个生成器来包装这个操作:

function * MyLogic(){
var serverData = yield myAjax();
console.log('MyLogic after myAjax');
console.log('serverStatus:%s',serverData.status);
}

使用的时候这样用:

var myLogic = MyLogic();
var promise = myLogic.next().value;
promise.then(function(serverData){
myLogic.next(serverData);
});

可以看到,我们这里的myAjax1()以及MyLogic()函数中,并没有使用回调,就完成了异步操作。

这里有几个值得注意的点:

  1. myAjax()函数返回的是一个Promise对象
  2. myLogic中的第一个语句,返回给外界的是myAjax()返回的Promise对象,等外界再次调用next()方法时将数据传进来,赋值给serverDate
  3. promise的状态是由第三段代码,在外部进行处理,完成的时候调用myLogic.next()方法并将serverData再传回MyLogic()

你一定会问,下面这个promise.done不就是回调操作么?Bingo!这正是精华所在!我们来看一下这段代码做了什么:

首先,myLogic.next()返回了一个Promise对象(promise),然后,promise.then中的回调函数所做的事情就是调用myLogic.next()方法就行了,除了调用next()方法,其它的什么事情都没有。此时,我们就会想到一个程序员特别喜欢的词,叫“封装”!既然这个回调函数只是调用myLogic.next()方法,那为什么不把它封装起来?

异步封装

首先,我们保持myAjax()MyLogic定义不变,而将myLogic.next()放到一个函数来调用,这个函数专门负责调用myLogic.next(),得到返回的Promise对象,然后在Promise被resolve的时候再次调用myLogic.next()

var myLogic = MyLogic();
 
function genRunner(){
 
// 调用next()获取promise
var yieldValue = myLogic.next();
var promise = yieldValue.value;
 
if(promise){
promise.then(function(data){
// promise被resolve的时候再次调用genRunner
// 以继续执行MyLogic中后面的逻辑
genRunner();
});
}
}

这样我们就把不停地调用myLogic.next()和不停地promise.then()的过程进行了封装。运行genRunner()跑一下:

MyLogic after myAjax1
Uncaught (in promise) TypeError: Cannot read property 'status' of undefined(…)

可见MyLogicyield后的语句的确被执行了,但是serverData却没有值,这是因为我们在调用myLogic.next()的时候没有把值传回去。稍微修改下代码:

// diff1: genRunner接受参数val
function genRunner(val){
 
// diff2: .next调用时把参数传过去,yield左边可以被赋值
var yieldValue = myLogic.next(val);
var promise = yieldValue.value;
 
if(promise){
promise.then(function(data){
// diff3: 调用genRunner时传递参数
genRunner(data);
});
}
}

这次一切都对了:

MyLogic after myAjax1
serverStatus:200

至此我们已经把封装最核心的部分抽离出来了,我们的业务代码MyLogic()已经是“异步操作,同步写法”,而我们亲眼见证了这一切是怎么办到的。那么接下来?为什么不再封装得更通用一些呢?

var genRunner = function(GenFunc){
 
return new Promise(function(resolve, reject){
 
var gen = GenFunc();
 
var innerRun = function(val){
 
var val = gen.next(val);
 
// 如果已经跑完了,则resolve
if(val.done){
resolve(val.value);
return;
}
// 如果有返回值,则调用`.then`
// 否则直接调用下一次innerRun()
// 为简单起见,假设有值的时候永远是promise
if(val.value){
val.value.then(function(data){
innerRun(data);
});
}else{
innerRun(val.value);
}
 
}
innerRun();
 
});
 
};

这里我们将刚刚看过的封装改成了innerRun(),并加上了自动调用。外面再封装了一层genRunner(),返回一个Promise。在genFunc全程调用完之后,Promise被resolve。

用起来大约是这样:

genRunner(function*(){
 
var serverData = yield myAjax();
console.log('MyLogic after myAjax');
console.log('serverStatus:%s',serverData.status);
 
}).then(function(message){
 
console.log(message);
 
});

生活真美好!

最后,以别人文章中的一段koa框架使用代码收尾吧:

var koa = require('koa'),
app = koa();
 
app.use(function *() {
 
// 这是这个例子中最重要的部分,我们进行了一系列异步操作,却没有回调
var city = yield geolocation.getCityAsync(this.req.ip);
var forecast = yield weather.getForecastAsync(city);
 
this.body = 'Today, ' + city + ' will be ' + forecast.temperature + ' degrees.';
 
});
 
app.listen(8080);

眼熟吗?koa就是像我们刚刚做的这样,封装了对生成器返回值的处理和调用next()方法的细节(这里的app.use()就像前面的genRunner()函数),使得我们的逻辑代码看起来是如此简单,这正是koa的伟大之处,也是ES6生成器这一特性能迅速引起如此多轰动的真正原因。

学习ES6生成器(Generator)的更多相关文章

  1. Python学习笔记 - 生成器generator

    #!/usr/bin/env python3 # -*- coding: utf-8 -*- # generator 生成器 L = [x * x for x in range(10)] print( ...

  2. ES6中的迭代器(Iterator)和生成器(Generator)

    前面的话 用循环语句迭代数据时,必须要初始化一个变量来记录每一次迭代在数据集合中的位置,而在许多编程语言中,已经开始通过程序化的方式用迭代器对象返回迭代过程中集合的每一个元素 迭代器的使用可以极大地简 ...

  3. ES6生成器函数generator

    ES6生成器函数generator generator是ES6新增的一个特殊函数,通过 function* 声明,函数体内通过 yield 来指明函数的暂停点,该函数返回一个迭代器,并且函数执行到 y ...

  4. 廖雪峰老师博客学习《通过生成器generator生成列表式杨辉三角》

    说明:这是我接触生成器概念后,自己对它的理解,可能比较表面,没深入理解,也可能有错误.后续校正错误认知,将有关generator作为一个tag了! 希望以后能活用. 先贴出自己写的triangles( ...

  5. ES6生成器基础

    ES6引进的最令人兴奋的特性就是一种新的函数生成方式,称为生成器(generator).名称有点奇怪,但是第一眼看上去行为更加奇怪.文章主要介绍生成器如何工作,然后让你明白为什么他们对于未来的JS会有 ...

  6. 【翻译】ES6生成器简介

    原文地址:http://davidwalsh.name/es6-generators ES6生成器全部文章: The Basics Of ES6 Generators Diving Deeper Wi ...

  7. Python高级编程之生成器(Generator)与coroutine(一):Generator

    转载请注明出处:点我 这是一系列的文章,会从基础开始一步步的介绍Python中的Generator以及coroutine(协程)(主要是介绍coroutine),并且详细的讲述了Python中coro ...

  8. TypeScript 迭代器(iterator)和生成器(generator)

    ⒈迭代器(iterator) 1.可迭代性 当一个对象实现了Symbol.iterator属性时,我们认为它是可迭代的. 一些内置的类型如 Array,Map,Set,String,Int32Arra ...

  9. 生成器generator和迭代器Iterator

    一.列表生成式       在学习生成器迭代器之前先了解一下什么是列表生成式,列表生成式是Python内置的非常简单却强大的可以用来创建list的生成式.什么意思?举个例子,如果想生成列表[0,1,2 ...

随机推荐

  1. Python标准模块--linecache

    1.模块简介 linecache主要用于缓存文件内容,如果下次继续读取该文件,则不需要打开文件,直接在缓存中获取该文件内容. 2.模块使用 模块的基本方法有getline,clearcache,get ...

  2. NodeJS POST Request Over JSON-RPC

    1.npm install art-template2.npm  install request3.在app.js中加入以下代码转html: var template = require('art-t ...

  3. PHP关于web页面交互内容

    学php学了有一段时间了总结总结给大家分享一下 PHP中的引用 第一段程序: <?php $first_name="firstName"; $first=&$firs ...

  4. 关于opencv中人脸识别主函数的部分注释详解。

    近段时间在搞opencv的视频人脸识别,无奈自带的分类器的准确度,实在是不怎么样,但又能怎样呢?自己又研究不清楚各大类检测算法. 正所谓,功能是由函数完成的,于是自己便看cvHaarDetectObj ...

  5. Hammer.js分析(三)——input.js

    input.js是所有input文件夹中类的父类,浏览器事件绑定.初始化特定的input类.各种参数计算函数. Input父类和其子类就是在做绑定事件,各种参数计算.整合.设置等返回自定义事件对象,交 ...

  6. java 线程的开始、暂停、继续

    Android项目中的一个需求:通过线程读取文件内容,并且可以控制线程的开始.暂停.继续,来控制读文件.在此记录下. 直接在主线程中,通过wait.notify.notifyAll去控制读文件的线程( ...

  7. 20个非常棒的jQuery倒计时脚本

    使用jQuery倒计时插件可能是最简单最好的方式添加动态和交互式倒数计时器到您的网站上.我相信你已经注意到了,例如倒计时功能运行网站,显示倒计时,直到一个大事件.当网站正在维护,告诉用户什么时候回来, ...

  8. Moon.Orm与其他Orm的技术对比

    有时候在思考大家为什么喜欢EF,为什么又出现这么多的Orm,为什么Nhiberate被人许多人接收又被许多人拒绝 最后发现结论:萝卜白菜各有所爱.适合自己的就是最好的. EF 微软团队支持(可谓强大的 ...

  9. [Web API] Web API 2 深入系列(5) 特性路由

    目录 1. 特性路由注册 2. 路由解析 - 生成DataTokens - 选择HttpController - 选择Action 特性路由的目的在于更好的提供restful架构的接口,最近好忙(懒) ...

  10. MVC学习系列8--分页和排序

    分页和排序,应该是软件开发中,需要必知必会的技能了,对于分页,网上很多教程,当然,别人终究是别人的,只有自己理解,会了,并且吸收之后,再用自己的语言,传授出来,这才是硬道理.好了,废话说多了.现在我们 ...