ES6异步操作-迭代器(Iterator)和生成器(Generator)

平常对数据进行迭代时使用较多的是循环语句,这就要求必须初始化一个变量,记录每一次迭代在数据集合中的位置。ES6的出现,让我们可以通过程序化的方式用迭代器对象返回迭代过程中集合中的每一个元素,从而极大简化数据操作。新的数组方法和新的集合类型(如Set集合、Map集合)都依赖迭代器实现,其他的如for/of循环、展开运算符(...)也都依赖了迭代器。本文介绍ES6中的迭代器(Iterator)和生成器(Generator)

迭代器

迭代器是一种特殊对象,它有一些专门为迭代过程设计的接口。所有迭代器都有一个next()方法,每次调用会返回一个结果对象,结果对象有两个属性:value属性表示下一个要返回的值;done属性是一个布尔值,如果没有更多数据可返回时返回true,否则返回false。

生成器

生成器是一种返回迭代器的函数,通过function关键字后的星号(*)来表示,函数中会用到新的关键字yield。星号可以紧挨着function关键字,也可以在中间添加一个空格

function *createIterator() {
  yield 1;
  yield 2;
  yield 3;
}
// 生成器函数调用方式和普通函数一样,只是返回的是一个迭代器
let iterator = createIterator()
console.log(iterator.next()) // {value: 1, done: false}
console.log(iterator.next()) // {value: 2, done: false}
console.log(iterator.next()) // {value: 3, done: false}
console.log(iterator.next()) // {value: undefined, done: true}
console.log(iterator.next()) // {value: undefined, done: true}

示例中,createIterator是一个生成器函数,yield关键字是ES6的新特性,可以通过它指定调用迭代器next()方法时的返回值和返回顺序。生成迭代器后,连续调用next()方法,前三次依次返回了{value: 1, done: false} {value: 2, done: false} {value: 3, done: false},而第四次和后面的调用,由于没有更多相关数据,所以返回了{value: undefined, done: true}

生成器函数中的yield语句,每次执行后函数会立即停止。示例中第一次调用next()方法后执行yield 1,直到再次调用next()方法才会继续执行yield 2语句。

yield关键字可以返回任何值或表达式,可以通过生成器函数批量的给迭代器添加元素

function *createIterator(items) {
  for (let i = 0; i < items.length; i++) {
    yield items[i]
  }
}

let iterator = createIterator([1,2,3])
console.log(iterator.next()) // {value: 1, done: false}
console.log(iterator.next()) // {value: 2, done: false}
console.log(iterator.next()) // {value: 3, done: false}
console.log(iterator.next()) // {value: undefined, done: true}
console.log(iterator.next()) // {value: undefined, done: true}

示例中,for循环不断从数组中生成新的元素放入迭代器中,每遇到一个yield语句循环都会停止;每次调用迭代器的next()方法,循环会继续运行并执行下一条yield语句

注意事项

  1. yield关键字只可在生成器内部使用,在其他地方使用会导致程序抛出错误
  2. 不能用箭头函数来创建生成器

生成器函数表达式

可以通过函数表达式来创建生成器,只需在function关键字和小括号中间添加一个星号(*)即可

let createIterator = function *(items) {
  for (let i = 0; i < items.length; i++) {
    yield items[i];
  }
}
let iterator = createIterator([1, 2, 3])

作为对象方法

由于生成器本身就是函数,因而可以将它们添加到对象中。

var o = {
  *createIterator(items) {
    for (let i = 0; i < items.length; i++) {
      yield items[i];
    }
  }
}
let iterator = o.createIterator([1, 2, 3])

状态机

生成器的一个常用功能是生成状态机

let state = function*(){
  while(1){
    yield 'A';
    yield 'B';
    yield 'C';
  }
}

let status = state();
console.log(status.next().value);//'A'
console.log(status.next().value);//'B'
console.log(status.next().value);//'C'
console.log(status.next().value);//'A'
console.log(status.next().value);//'B'

可迭代对象

可迭代对象具有Symbol.iterator属性,是一种与迭代器密切相关的对象。Symbol.iterator可以通过指定的函数返回一个作用于附属对象的迭代器。ES6中的数组、Set集合、Map集合以及字符串,都有默认的迭代器。

注意: 由于生成器默认会为Symbol.iterator属性赋值,因此所有通过生成器创建的迭代器都是可迭代对象

ES6中的for/of循环同样利用了迭代器功能,每执行一次循环会调用可迭代对象的next()方法,并把迭代器返回的结果对象中的value属性存储在一个变量中,直到done属性为true退出循环。

let values = [1,2,3]
for (let item of values) {
  console.log(item)
}
// 1
// 2
// 3

for/of循环通过调用values数组的Symbol.iterator方法来获取迭代器(这一过程是在JS引擎背后完成的),然后每次循环都调用一次next()方法,直到done属性为true退出循环。

注意: 如果将for/of语句用于不可迭代对象上会导致程序抛出错误,比如null或undefined

访问默认迭代器

可以通过Symbol.iterator来访问对象默认的迭代器

let values = [1, 2, 3];
let iterator = values[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

由于具有Symbol.iterator属性的对象都有默认的迭代器,因此可以用它来检测对象是否为可迭代对象

function isIterable(obj) {
  return typeof obj[Symbol.iterator] === 'function'
}

console.log(isIterable(new Set())) // true
console.log(isIterable(new WeakSet())) // false

创建可迭代对象

默认情况下,开发者定义的对象都是不可迭代对象,但如果给Symbol.iterator属性添加一个生成器,则可以将其变为可迭代对象

let obj = {
  items: [1,2,3],
  *[Symbol.iterator]() {
    for(let item of this.items) {
      yield item;
    }
  }
}

for(let i of obj){
  console.log(i)
}
// 1
// 2
// 3

示例中,创建了一个生成器并把它赋值给对象的Symbol.iterator属性。生成器通过for/of循环迭代items中的值,并通过yield返回每一个值

展开运算符

展开运算符默认会调用对象Symbol.iterator属性对应的迭代函数,所以可以用于任何可迭代对象。

let a1 = [1,2,3], a2 = [4,5,6], a3 = [0,...a1, ...a2]
console.log(a3.length, a3) // 7 [0, 1, 2, 3, 4, 5, 6]

let set = new Set([1,2,3]), arr = [...set]
console.log(arr) // [1,2,3]

内建迭代器

通常只有自定义对象才需要自定义迭代器,ES6中很多内建类型都默认提供了内建迭代器

集合对象迭代器

在ES6中有3种类型的集合对象:数组、Map集合与Set集合,为了更好地访问对象中的内容,这3种对象都内建了以下三种迭代器

entries() 返回一个迭代器,其值为多个键值对
values() 返回一个迭代器,其值为集合的值
keys() 返回一个迭代器,其值为集合中的所有键名

关于Set集合和Map集合的介绍移步于此

NodeList迭代器

DOM标准中有一个NodeList类型,document对象中的所有元素都用这个类型来表示。自从ES6添加了默认迭代器后,DOM定义中的NodeList类型其行为与数组的默认迭代器完全一致,所以可以将NodeList应用于for-of循环及其他支持对象默认迭代器的地方

var divs = document.getElementsByTagName('div');
for (let div of divs) {
    console.log(div.id);
}

高级迭代器

迭代器传递参数

如果给迭代器的next()方法传递参数,则这个参数的值就会替代生成器内部上条yield语句的返回值。而如果要实现更多像异步编程这样的高级功能,那么这种给迭代器传值的能力就变得至关重要

function *createIterator() {
  let first = yield 1;
  let second = yield first + 2; // 4 + 2
  yield second + 3; // 5 + 3
}
let iterator = createIterator();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next(4)); // { value: 6, done: false }
console.log(iterator.next(5)); // { value: 8, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

第一次调用next()方法时无论传入什么参数都会被丢弃。由于传给next()方法的参数会替代上一次yield的返回值,而在第一次调用next()方法前不会执行任何yield语句,因此在第一次调用next()方法时传递参数是毫无意义的

第二次调用next()方法传入数值4作为参数,它最后被赋值给生成器函数内部的变量first。

第三次调用next()方法时传入数值5作为参数,这个值被赋值给second,最后用于第三条yield语句并最终返回数值8

在迭代器中抛出错误

通过throw()方法,当迭代器恢复执行时可令其抛出一个错误。

function *createIterator() {
  let first = yield 1;
  let second = yield first + 2;
  yield second + 3;
}
let iterator = createIterator();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next(4)); // { value: 6, done: false }
console.log(iterator.throw(new Error("Boom"))); // 从生成器中抛出了错误

示例中,前两个表达式正常求值,而调用throw()方法后,执行完yield first + 2;便抛出错误,此时并没有对let second求值,所以yield second + 3;永远不会执行

可以通过try-catch语句捕获这些错误

function *createIterator() {
  let first = yield 1;
  let second;
  try {
    second = yield first + 2;
  }catch(e) {
    second = 6;
  }
  yield second + 3;
}
let iterator = createIterator();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next(4)); // { value: 6, done: false }
console.log(iterator.throw(new Error(Boom))); // { value: 9, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

示例中,当抛出错误后second会被重新赋值为6,所以yield second + 3;最后可以执行

next()和throw()就像是迭代器的两条指令,调用next()方法命令迭代器继续执行,调用throw()方法也会命令迭代器继续执行,但同时也抛出一个错误,只要能正确处理异常迭代器就会继续执行

生成器返回语句

由于生成器也是函数,因此可以通过return语句提前退出函数执行,return语句可以指定一个返回值。生成器中return表示操作已经完成,所以done属性会被设置为true,value属性就是对应的返回值

function *createIterator() {
  let first = yield 1;
  return 100;
  let second = yield first + 2;
  yield second + 3;
}
let iterator = createIterator();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 100, done: true }
console.log(iterator.next()); // { value: undefined, done: true }

委托生成器

可以通过委托生成器把多个生成器合并成一个。只需要创建一个生成器,然后在yield语句后添加其他生成器,就可以将生成数据的过程委托给其他生成器

function *createNumberIterator() {
  yield 1;
  yield 2;
}
function *createColorIterator() {
  yield 'red';
  yield 'green';
}
function *createCombinedIterator() {
  yield *createNumberIterator();
  yield *createColorIterator();
}
var iterator = createCombinedIterator();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: "red", done: false }
console.log(iterator.next()); // { value: "green", done: false }
console.log(iterator.next()); // { value: undefined, done: true }

示例中的生成器createCombinedIterator(),先后委托了另外两个生成器createNumberlterator()和createColorlterator()。根据迭代器的返回值来看,它就像是一个完整的迭代器,可以生成所有的值

异步任务执行

由于生成器支持在函数中暂停代码执行,因而可以使用它来处理异步任务,这也是生成器的强大之处

执行异步操作的传统方式是调用一个函数,函数执行结束后执行对应的回调函数

let fs = require('fs');
fs.readFile('config.json', function(err, contents) {
  if(err) throw err;
  doSomething(contents.toString());
})

readFile是一个异步操作,读取config.json中的数据,操作结束后如果有错误,直接抛出错误,没有错误就可以处理返回的内容了。如果异步任务比较少,这种方式还是不错的,但是如果回调中还有一系列其他的异步任务,就需要层层嵌套下去,这不仅使代码看起来混乱,而且更容易出错。下面借助生成器改变多层嵌套下的回调问题

简单任务执行器

利用yield语句会暂停函数执行这一特性,我们创建一个函数,在函数中调用生成器相应的迭代器,从而实现异步调用next()方法

function run(task) {
  let iterator = task(); // 生成器函数task的迭代器
  let result = iterator.next(); // 启动任务
  function step() { // 递归调用step函数,保持对next()的调用
    if(!result.done) {
      result = iterator.next();
      step();
    }
  }
  step(); // 启动处理过程
}

使用run函数,执行一个包含多条yield语句的生成器

run(function *(){
  yield console.log(1);
  yield console.log(2);
  yield console.log(3);
})

这个示例的输出结果依次是1、2、3,第一次调用next()方法后,返回的结果存储到result中稍后继续使用,通过判断done的值决定是否继续调用next()方法。下一步将在迭代器与调用者之间互相传值。

向任务执行器传递数据

可以把值通过迭代器的next()方法传入,作为yield的生成值供下次调用。即把result.value传入next()方法

function run(task) {
  let iterator = task(); // 生成器函数task的迭代器
  let result = iterator.next(); // 启动任务
  function step() { // 递归调用step函数,保持对next()的调用
    if(!result.done) {
      result = iterator.next(result.value);
      step();
    }
  }
  step(); // 启动处理过程
}
run(function *(){
  let v = yield 1; 
  let v2 = yield v + 2;
  console.log(v, v2) // 1 3
})

数值1取自yield 1调用next()方法后回传给变量v的值,数值3取自yield v + 2调用next()方法后回传给变量v2的值。既然可以传参了,现在离实现异步调用只有一步之遥了

异步任务执行器

function run(task) {
  let iterator = task(); // 生成器函数task的迭代器
  let result = iterator.next(); // 启动任务
  function step() { // 递归调用step函数,保持对next()的调用
    if(!result.done) {
      // 如果是函数,执行异步中的回调
      if (typeof result.value === 'function') {
        result.value(function(err, data) {
          if(err) {
            result = iterator.throw(err);
            return;
          }
          result = iterator.next(data);
          step();
        })
      }else{
        result = iterator.next(result.value);
        step();
      }
    }
  }
  step(); // 启动处理过程
}
// 模拟异步函数
function f1() {
  return function(callback) {
    setTimeout(function() {
      callback(null, 'hello')
    }, 100)
  }
}

function f2() {
  return function(callback) {
    setTimeout(function() {
      callback(null, 'world')
    }, 50)
  }
}

run(function *(){
  let v1 = yield f1();
  let v2 = yield f2();
  console.log(v1, v2); // hello world
})

这次倒着分析,从结果分析实现过程。f1和f2是两个函数,调用后返回了一个新的函数,这个新函数接收一个回调函数作为参数,而这个回调函数的执行是异步的。在run函数内部对result.value进行了判断,如果是一个函数就执行这个函数,并把一个函数作为参数传给callback。callback(null, 'hello')中的null就是接收这个函数的占位符,hello就是函数中的data,最后通过iterator.next(data)把值传递回去。如果不是函数,直接把值传递给next()方法即可

需要注意的是,异步函数的模式必须是类似f1、f2这种定义,即需要在外层包一层函数,并且返回一个可以接收回调函数的函数

在Nodejs中异步读取文件可以这样做

function readFile(filename) {
  return function(callback) {
    fs.readFile(filename, callback)
  }
}

run(function*() {
  let contents = yield readFile('config.json');
  doSomething(contents.toString());
});

虽然功能是实现了,但是代码确实不好理解。后面会介绍Promise和ES7中的async-await,让异步编程更简单

posted @ 2021-09-29 13:00  wmui  阅读(277)  评论(0编辑  收藏  举报