JavaScript Generators指南

图片描述
最近,为了更好地理解Redux Sagas的工做原理,我重学了JavaScript generators的知识,我把从网上收集到的各类知识点浓缩到一篇文章里,我但愿这篇文章既通俗易懂,又足够严谨,能够做为初学者的generators使用指南。前端

简介

JavaScript在ES6时引入了生成器。生成器函数与常规函数相似,除了能够暂停和恢复它们这一点之外。生成器也与迭代器密切相关,由于生成器对象就是迭代器。
在JavaScript中,函数调用后一般不能暂停或中止。(是的,异步函数在等待await语句时暂停,可是异步函数在ES7时才引入。此外,异步函数是创建在生成器之上的。)一个普通函数只有在返回或抛出错误时才会结束。算法

function foo() {
  console.log('Starting');
  const x = 42;
  console.log(x);  
  console.log('Stop me if you can');  
  console.log('But you cannot');
 }

相反,生成器容许咱们在任意断点处暂停执行,并从同一断点恢复执行。编程

生成器和迭代器

来自MDN数组

在JavaScript中,迭代器是一个对象,它定义一个序列,并在终止时可能返回一个返回值。 >更具体地说,迭代器是经过使用 next() 方法实现 Iterator protocol >的任何一个对象,该方法返回具备两个属性的对象: value,这是序列中的 next 值;和 done, 若是已经迭代到序列中的最后一个值,则它为 true 。若是 value 和 done 一块儿存在,则它是迭代器的返回值。

所以,迭代器的本质就是:promise

  • 定义序列的对象
  • 有一个next()方法…
  • 返回一个具备两个属性的对象:value和done

是否须要生成器来建立迭代器?不。事实上,咱们已经可使用闭包pre-ES6建立一个无限的斐波那契数列,以下例所示:闭包

var fibonacci = {
  next: (function () {
    var pre = 0, cur = 1;
    return function () {
      tmp = pre;
      pre = cur;
      cur += tmp;
      return cur;
    };
  })()
};

fibonacci.next(); // 1
fibonacci.next(); // 2
fibonacci.next(); // 3
fibonacci.next(); // 5
fibonacci.next(); // 8

关于生成器的好处,我将再次引用MDN异步

虽然自定义迭代器是一个有用的工具,可是因为须要显式地维护它们的内部状态,建立它们须要咱们仔细地编程。生成器函数提供了一个强大的替代方法:它们容许咱们经过编写一个执行不是连续的函数来定义迭代算法。
换句话说,使用生成器建立迭代器更简单(不须要闭包!),这意味着出错的可能性更小。
生成器和迭代器之间的关系就是生成器函数返回的生成器对象是迭代器。

语法

生成器函数使用function *语法建立,并使用yield关键字暂停。
最初调用生成器函数并不执行它的任何代码;相反,它返回一个生成器对象。该值经过调用生成器的next()方法来使用,该方法执行代码,直到遇到yield关键字,而后暂停,直到再次调用next()。ide

function * makeGen() {
  yield 'Hello';
  yield 'World';
}

const g = makeGen(); // g is a generator
g.next(); // { value: 'Hello', done: false }
g.next(); // { value: 'World', done: false }
g.next(); // { value: undefined, done: true }

在上面的最后一个语句以后重复调用g.next()只会返回(或者更准确地说,产生)相同的返回对象:{ value: undefined, done: true }。函数

yield暂停执行

你们可能会注意到上面的代码片断有一些特殊之处。第二个next()调用生成一个对象,该对象的属性为done: false,而不是done: true。
既然咱们正在生成器函数中执行最后一条语句,那么done属性不该该为true吗?并非的。当遇到yield语句时,它后面的值(在本例中是“World”)被生成,执行暂停。所以,第二个next()调用暂停在第二个yield语句上,所以执行尚未完成—只有在第二个yield语句以后执行从新开始时,执行才算完成(即done: true),而且再也不运行代码。
咱们能够将next()调用看做是告诉程序运行到下一个yield语句(假设它存在)、生成一个值并暂停。程序在恢复执行以前不会知道yield语句以后没有任何内容,而且只能经过另外一个next()调用恢复执行。工具

yield和return

在上面的示例中,咱们使用yield将值传递给生成器外部。咱们也可使用return(就像在普通函数中同样);可是,使用return能够终止执行并设置done: true。

function * makeGen() {
  yield 'Hello';
  return 'Bye';
  yield 'World';
}

const g = makeGen(); // g is a generator
g.next(); // { value: 'Hello', done: false }
g.next(); // { value: 'Bye', done: true }
g.next(); // { value: undefined, done: true }

由于执行不会在return语句上暂停,并且根据定义,在return语句以后不能执行任何代码,因此done被设置为true。

yield:next方法的参数

到目前为止,咱们一直在使用yield传递生成器外部的值(并暂停其执行)。
然而,yield其实是双向的,而且容许咱们将值传递到生成器函数中。

function * makeGen() {
  const foo = yield 'Hello world';
  console.log(foo);
}

const g = makeGen();
g.next(1); // { value: 'Hello world', done: false }
g.next(2); // logs 2, yields { value: undefined, done: true }

等一下。不该该是"1"打印到控制台,可是控制台打印的是"2"?起初,我发现这部分在概念上与直觉相反,由于我预期的赋值foo = 1。毕竟,咱们将“1”传递到next()方法调用中,从而生成Hello world,对吗?
但事实并不是如此。传递给第一个next(...)调用的值将被丢弃。除了这彷佛是ES6规范以外,实际上没有其余缘由.从语义上讲,第一个next方法用来启动遍历器对象,因此不用带有参数。
我喜欢这样对程序的执行进行合理化:

  • 在第一个next()调用时,它将一直运行,直到遇到yield 'Hello world',在此基础上生成{ value: 'Hello world', done: false }和暂停。就是这么回事。正如你们所看到的,传递给第一个next()调用的任何值都是不会被使用的(所以被丢弃)。
  • 当再次调用next(...)时,执行将恢复。在这种状况下,执行须要为常量foo分配一些值(由yield语句决定)。所以,咱们对next(2)的第二次调用赋值foo=2。程序不会在这里中止—它会一直运行,直到遇到下一个yield或return语句。在本例中,没有更多的yield,所以它记录2并返回undefined的done: true。在生成器使用异步由于yield是一个双向通道,容许信息在两个方向上流动,因此它容许咱们以很是酷的方式使用生成器。到目前为止,咱们主要使用yield在生成器以外传递值。可是咱们也能够利用yield的双向特性以同步方式编写异步函数。

使用上面的概念,咱们能够建立一个相似于同步代码但实际上执行异步函数的基本函数:

function request(url) {
  fetch(url).then(res => {
    it.next(res); // Resume iterator execution
  });
}

function * main() {
  const rawResponse = yield request('https://some-url.com');
  const returnValue = synchronouslyProcess(rawResponse);
  console.log(returnValue);
}

const it = main();
it.next(); // Remember, the first next() call doesn't accept input

这是它的工做原理。首先,咱们声明一个request函数和main生成器函数。接下来,经过调用main()建立一个迭代器it。而后,咱们从调用it.next()开始。
在第一行的function * main(),在yield request('https://some-url.com')以后执行暂停。request()隐式地返回undefined,所以咱们实际上生成了undefined值,但这并不重要—咱们没有使用该值。
当request()函数中的fetch()调用完成时,it.next(res)将会被调用并完成下列两件事:
it继续执行;和
it将res传递给生成器函数,该函数被分配给rawResponse
最后,main()的其他部分将同步完成。
这是一个很是基础的设置,应该与promise有一些类似之处。有关yield和异步性的更详细介绍,请参阅此文。

生成器是一次性

咱们不能重复使用生成器,但能够从生成器函数建立新的生成器。

function * makeGen() {
  yield 42;
}

const g1 = makeGen();
const g2 = makeGen();
g1.next(); // { value: 42, done: false }
g1.next(); // { value: undefined, done: true }
g1.next(); // No way to reset this!
g2.next(); // { value: 42, done: false }
...
const g3 = makeGen(); // Create a new generator
g3.next(); // { value: 42, done: false }

无限序列

迭代器表示序列,有点像数组。因此,咱们应该可以将全部迭代器表示为数组,对吧?
然而,并非的。数组在建立时须要当即分配,而迭代器是延迟使用的。数组是迫切须要的,由于建立一个包含n个元素的数组须要首先建立/计算全部n个元素,以便将它们存储在数组中。相反,迭代器是惰性的,由于序列中的下一个值只有在使用时才会建立/计算。
所以,表示无限序列的数组在物理上是不可能的(咱们须要无限内存来存储无限项!),而迭代器能够轻松地表示(而不是存储)该序列。
让咱们建立一个从1到正无穷数的无穷序列。与数组不一样,这并不须要无限内存,由于序列中的每一个值只有在使用时才会懒散地计算出来。

function * makeInfiniteSequence() {
  var curr = 1;
  while (true) {
    yield curr;
    curr += 1;
  }
}

const is = makeInfiniteSequence();
is.next(); { value: 1, done: false }
is.next(); { value: 2, done: false }
is.next(); { value: 3, done: false }
... // It will never end

有趣的事实:这相似于Python生成器表达式vs列表理解。虽然这两个表达式在功能上是相同的,可是生成器表达式提供了内存优点,由于值的计算是延迟的,而列表理解则是当即计算值并建立整个列表。

看以后

点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓-_-)
关注公众号「新前端社区」,享受文章首发体验!
每周重点攻克一个前端技术难点。

相关文章
相关标签/搜索