📒【异步】7. 生成器 & 迭代器 & for..of

ES6生成器(generator)让一种顺序、看似同步的异步流程控制表达风格成为可能。

生成器

生成器是一类特殊的函数,可以一次或多次启动和停止,并不一定非得要完成。

  1. 生成器本身也是一个函数,因此它可以接受参数,也能够返回值。
function *foo(x, y){
  return x * y;
}

// 构造一个迭代器it来控制这个生成器
var it = foo(6,7);
var res = it.next();
console.log(res); //{ value: 42, done: true }

生成器和普通函数在调用上的一个区别: foo(6,7),生成器 *foo(...) 并没有像普通函数一样实际运行。
事实上,我们只是创建了一个迭代器对象,把它赋给变量it,用于控制生成器*foo(...)。调用it.next(),指示生成器从当前位置开始继续运行,停在下一个yield处或直到生成器结束。
next(...) 调用的结果是一个对象,它有一个value属性,持有从*foo(...)返回的值。

  1. 生成器提供内建消息输入输出能力,通过yieldnext()实现。
function *foo(x){
  var y = x * (yield);
  return y;
}
var it = foo(6); // 传入6作为参数x
it.next(); // 启动 *foo(...)

var res = it.next(7);
console.log(res); //{ value: 42, done: true }

第一次调用it.next();时,在*foo(...)内部开始执行语句var y = x ..,随后遇见yield表达式。它会在这一点上暂停*foo(...),并在本质上要求调用代码为yield表达式提供一个结果值
调用it.next(7) 将值7作为被暂停的yield表达式的结果。所以,这时赋值语句实际上就是var y = 6 * 7

  1. 一般来讲,需要的next(...)调用要比yield语句多一个。
    🤔️ ?因为第一个next(...)总是启动一个生成器,并运行到第一个yield处。是第二个next(...)调用完成第一个被暂停的yield表达式。

  2. 每次构建一个迭代器,实际上就隐式构建了生成器的一个实例,通过这个迭代器来控制的是这个生成器的实例。
    同一个生成器的多个实例可以同时运行,它们甚至可以彼此交互。

迭代器

⛲️ 场景:假定要生成一系列值,其中每个值都与前面一个有特定都关系。要实现这一点,就需要一个有状态的生产者能够记住其生成的最后一个值!
😄 1. 直接使用函数闭包实现:

var gimmeSomething = (function(){
  var nextVal;

  return function(){
    if(nextVal === undefined){
      nextVal = 1;
    }else{
      nextVal = (3 * nextVal) + 6;
    }
    return nextVal;
  }
})();
console.log(gimmeSomething()); //1
console.log(gimmeSomething()); //9
console.log(gimmeSomething()); //33
console.log(gimmeSomething()); //105

😄 2. 通过迭代器来解决
迭代器是一个定义良好的接口,用于从一个生产者一步步得到一系列值,每次想要从生成者得到下一个值的时候就调用next()。每次调用 next() 都会返回一个结果对象,该结果对象有两个属性,value 表示当前的值,done 表示遍历是否结束。

var something = (function(){
  var nextVal;

  return {
    [Symbol.iterator]: function(){ return this; }, //for..of循环需要
    next: function(){ //标准迭代器接口方法
      if(nextVal === undefined){
        nextVal = 1;
      }else{
        nextVal = (3 * nextVal) + 6;
      }
      return { done: false, value: nextVal };
      //done标识迭代器的完成状态,value放置迭代值
    }
  }
})();
console.log(something.next().value); //1
console.log(something.next().value); //9
console.log(something.next().value); //33
console.log(something.next().value); //105

for(var v of something){
  console.log(v);
  if(v > 500){ //避免死循环
    break;
  }
} // 321 969

for..of

ES6 新增了一个 for..of 循环,可以通过原生循环语法自动迭代标准迭代器。因为我们的迭代器 something 总是返回 done: false,因此这个for..of循环将永远运行下去,为避免死循环放了一个 break
for..of 循环在每次迭代中自动调用 next() ,它不会向next() 传入任何值,并且会在接收到 done:true 之后自动停止。
📖 除了构造自己的迭代器,许多JavaScript的内建数据结构(从ES6开始)都默认部署了Symbol.iterator属性(即默认迭代器),比如

  • 数组
  • Set
  • Map
  • 类数组对象,如 arguments 对象、DOM NodeList 对象
  • Generator对象
  • 字符串
for(var v of [1,2,3,4,5]){
  console.log(v)
}
// 1,2,3,4,5

⚠️ 一般的 object 没有像 array 一样有默认的迭代器。

iterable

iterable 可迭代,指一个包含可以在其值上迭代的迭代器的对象。

📖 ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说,一个数据结构只要具有 Symbol.iterator 属性,就可以认为是"可遍历的"(iterable)。

从ES6开始,从一个iterable中提取迭代器的方法是:iterable 必须支持一个函数,其名称是专门的ES6符号值 Symbol.iterator 。调用这个函数时,它会返回一个迭代器。通常每次调用都会返回一个全新的迭代器。for..of 遍历的其实是对象的 Symbol.iterator 属性。

异步迭代生成器

⛲️ 用生成器来表达异步任务流程控制:

function foo(x, y) {
  ajax(`http://some.url.1?x=${x}&y=${y}`, function(err, data){
    if(err){
      it.throw(err);
    }else{
      it.next(data);
    }
  });
}
function *main(){
  try {
    var text = yield foo(11, 12);
    console.log(text);
  } catch (error) {
    console.log(error);
  }
}

var it = main();
it.next();

回想使用回调的时候,下面代码几乎不能实现!

var data = ajax("...url 1 ...");
console.log(data);

二者区别在于生成器中使用了 yield,这一点使得我们看似阻塞同步的代码,实际上并不会阻塞整个程序,它只是暂停或阻塞了生成器本身的代码。
错误处理:
🤔️ Q:生成器*main内部的try..catch是如何工作的呢?调用foo(..)是异步完成的,try..catch 不是无法捕获异步错误吗?
😯 A:yield 让赋值语句暂停来等待 foo(..) 完成,使得响应完成后可以被赋给textyield的暂停也使得生成器能够捕获错误。
生成器yield暂停的特性意味着我们不仅能够从异步函数调用得到看似同步的返回值,还可以同步捕获来自这些异步函数调用的错误!

生成器 + Promise 协作运作模式

ES6中最完美的世界就是生成器(看似同步的异步代码)和 Promise(可信任可组合)的结合。
⛲️ Ajax调用返回一个promise,再外面包一层通过生成器将它yield出来,然后迭代器控制代码就可以接收到这个promise了。迭代器侦听promise的决议(完成或拒绝),然后要么使用完成消息恢复生成器运行,要么向生成器抛出一个带拒绝原因的错误。

function foo(x,y){
  return request(`http://some.url.1?x=${x}&y=${y}`)
}
function *main(){
  try {
    var text = yield foo(11, 31);
    console.log(text);
  } catch (error) {
    console.log(error);
  }
}

//运行
var it = main();
var p = it.next().value;
// 等待promise决议
p.then(function(text){
  it.next(text);
},function(err){
  it.throw(err);
})

async/await

function foo(x,y){
  return request(`http://some.url.1?x=${x}&y=${y}`)
}
async function main(){
  try {
    var text = await foo(11, 31);
    console.log(text);
  } catch (error) {
    console.error(error);
  }
}
main();

可以看到,main()不再被声明为生成器函数了,它现在是一类新的函数,async函数;我们不再yield出Promise,而是用await等待它决议。
如果你await一个Promise,async函数就会自动获知要做什么,它会暂停这个函数(就像生成器一样),知道Promise决议。
调用一个像main()这样的async函数会自动返回一个Promise。在函数完全结束之后,这个promise会决议。

其他

ES6 系列之 Generator 的自动执行
ES6 系列之我们来聊聊 Async
ES6系列之异步处理实战

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345