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

2020-08-06  本文已影响0人  BubbleM

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属性(即默认迭代器),比如

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系列之异步处理实战

上一篇下一篇

猜你喜欢

热点阅读