JS生成器简介

更新日期: 2021-11-18阅读: 1.2k标签: Generator

一. 单线程的 JS

var a = 3;
var b = 2;

function foo() {
  a = a * 2;
  bar();
  b++;
  console.log(a, b); // 7, 5
}

function bar() {
  a++;
  b = b * 2;
}

上面代码中 bar 函数很直观地在 b++ 与 a * 2 之间运行,现在假设一下,bar 现在不存在于 foo 函数里,那么怎么才能使单线程的 JS 中断 foo 函数去执行bar ,然后再回来继续执行自己没有执行的代码呢

二. yield 暂停

对于上面的问题,只要我们能够通知 foo 在某个地方暂停下来就可以实现这样的需求

var a = 3;
var b = 2;

function* foo() {
  // 声明生成器
  a = a * 2;
  yield;
  b++;
  console.log(a, b);
}

function bar() {
  a++;
  b = b * 2;
}
有些地方使用的是function *foo(){...} 的形式

那么现在要如何运行代码才能达到想要的效果呢

/* 构造一个迭代器it来控制生成器foo,此时foo完全没有被执行 */
var it = foo();

/* foo开始执行,一直到遇到yield才停止,此时a = 6, b = 2 */
it.next();
console.log(a, b); // 6, 2

/* foo让出线程,执行bar,此时a = 7, b = 4 */
bar();
console.log(a, b); // 7, 4

/* foo继续执行,此时a = 7, b = 5 */
it.next(); // 7, 5

生成器是一类特殊的函数,可以一次或多次启动与停止,而且不一定非要完成

三. 输入与输出

生成器特殊归特殊,它始终都是一个函数,依然具有一些函数的基本特性,例如接受参数(输入),返回值(输出)

function* foo(x, y) {
  return x * y;
}

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

next 的调用结果是一个对象,拥有一个 value 属性,持有从 foo 返回的值,也就是说,yield 会导致生成器在执行过程中发送出一个值,有点类似上面的return

3.1 迭代消息传递

除了能接收参数并提供返回值,生成器还提供了更强大的内建消息输入输出能力,通过 yield 和 next 实现

function* foo(x) {
  var y = x * (yield);
  return y;
}

var it = foo(6);

it.next();

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

首先 6 作为参数 x,调用it.next() ,启动foo

在foo 内部,执行语句var y = x... 时遇到yield 表达式,在此处(赋值语句中间)暂停foo ,并要求调用代码为yield 的表达式提供一个结果值,接下来调用it.next(7) ,这一句把 7 传回作为被暂停的yield 表达式的结果

3.2 从两个视角看next 与yield

一般来讲,next 的数量要比yield 的数量多一个,这在理解代码时会给人一种不协调不匹配的感觉

只考虑生成器代码

var y = x * yield;
return y;

第一个yield 提出一个问题:我这里应该插入什么值?

谁来回答这个问题呢,第一个next 已经运行使得生成器启动并运行到此处,它显然无法回答这个问题,因此必须由第二个next 调用回答第一个yield 提出的问题

这就是不匹配——第二个next对第一个yield?

接下来转换一下视角,从迭代器角度看问题

在此之前需要再次解释以下yield的消息传递,它是双向的,前面我们只提了next向暂停的yield发送值,下面看一下yield发出消息供next使用

function* foo(x) {
  var y = x * (yield "hello world");
  return y;
}

var it = foo(6);

/* 第一个next不传入任何东西,实际上就算传了也会被浏览器悄咪咪丢掉 */
var res = it.next();
console.log(res); // { value: 'hello world', done: false }

/* 给等待的yield传一个7 */
res = it.next(7);
console.log(res); // { value: 42, done: true }

第一个next 调用提出一个问题:foo要给我的下一个值是什么?

谁来回答呢?第一个yield "hello world" 表达式

这里没有不匹配的问题

但是,对于最后一个next ,也就是it.next(7) 提出的foo要给我的下一个值是什么的问题,没有yield 来回答它了,那么由谁来回答呢

答案是return 语句

如果生成器中没有return,则会有隐式的return undefined 来回答

四. 多个迭代器

语法使用的方面来看,通过一个迭代器控制生成器的时候,似乎是在控制声明的生成器函数本身,但有一个细节需要注意,每次构建一个迭代器,实际上就隐式构建了生成器的一个实例,通过这个迭代器来控制其对应的生成器实例

同一个生成器的多个实例可以同时运行,甚至彼此交互

function* foo(i) {
  var x = yield 2;
  z++;
  var y = yield x * z;
  console.log({ i, x, y, z });
}

var z = 1;
var it1 = foo(1);
var it2 = foo(2);

var val1 = it1.next().value;
var val2 = it2.next().value;
console.log({ val1, val2 }); // { val1: 2, val2: 2 }

val1 = it1.next(val2 * 10).value; // z = 2, 1中x = 20, val1 = x * z = 40
val2 = it2.next(val1 * 5).value; // z = 3, 2中x = 200, val2 = x * z = 600
console.log({ val1, val2 }); // { val1: 40, val2: 600 }

it1.next(val2 / 2); // { i: 1, x: 20, y: 300, z: 3 }
it2.next(val1 / 4); // { i: 2, x: 200, y: 10, z: 3 }

再来看一个例子

var a = 1;
var b = 2;

function foo() {
  a++;
  b = b * a;
  a = b + 3;
}

function bar() {
  b--;
  a = 8 + b;
  b = a * 2;
}

上面是一串普通的 js 代码,对于普通的 JS 函数,显然要么先执行 foo,要么先执行 bar,二者不能交替执行

但是,使用生成器的话,交替执行显然是可能的

var a = 1;
var b = 2;

function* foo() {
  a++;
  yield;
  b = b * a;
  a = (yield b) + 3;
}

function* bar() {
  b--;
  yield;
  a = (yield 8) + b;
  b = a * (yield 2);
}

根据每一步调用的相对顺序的不同,上面的程序能产生多种不同的结果

首先构建一个辅助函数来控制迭代器

function step(gen) {
  // 初始化一个生成器来创建迭代器it
  var it = gen();
  var last;

  // 返回一个函数,每次被调用都会将迭代器向前迭代一步,前面yield发出的值会在下一步发送回去
  return function () {
    // 不管yield出来的是什么下一次都把它原原本本的传回去
    last = it.next(last).value;
  };
}

下面我们先从最基本的情况开始,让 foo 在 bar 之前执行结束

var s1 = step(foo);
var s2 = step(bar);

// 执行foo
s1();
s1();
s1();

// 执行bar
s2();
s2();
s2();
s2();

console.log(a, b); // 11, 22

然后我们使二者交替进行

var s1 = step(foo);
var s2 = step(bar);

s2();
// b--, b = 1, 遇到yield,暂停!last接受第一个yield发出的值undefined

s2();
// 第一个yield返回值为undefined(无影响),执行a = ...遇到yield,暂停!last 接受第二个yield发出的值8

s1();
/* a++, a = 2, 遇到yield,暂停!last接受第一个yield发出的值undefined */

s2();
// 第二个yield返回值为8,a = 8 + b = 8 + 1 = 9, 执行b = 9 * ...遇到yield,暂停!last 接受第三个yield发出的值2

s1();
/* 第一个yield返回值为undefined(无影响),b = b * a = 1 * 9 = 9, 执行a = ..., 遇到yield,暂停!last接受第二个yield发出的值b,也就是9 */

s1();
/* 第二个yield返回值为9,a = 9 + 3 = 12,s1执行完毕,foo结束 */

s2();
// 第三个yield返回值为2,b = 9 * 2 = 18,s2执行完毕,bar结束

console.log(a, b); // 12, 18

五. 迭代器与生成器

前面说了那么多“迭代器”与“生成器”,现在具体来谈一谈这二者是什么东东

5.1 迭代器

5.1.1 iterable

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

从 ES6 开始,从一个 iterable 中提取迭代器的方法是:iterable 必须支持以后函数,其名称是Symbol.iterable ,调用这个函数时,它会返回一个迭代器,通常每次调用会返回一个全新的迭代器

后面我们根据例子具体分析

5.1.2 生产者与迭代器

假定现在你要产生一系列值,每一个值都与前面一个有特定的关系,要实现这一点,需要一个有状态的生产者能记住其生成的最后一个值

先用闭包整一个

var getSomething = (function () {
  var nextVal;
  return function () {
    if (nextVal === undefined) nextVal = 1;
    else nextVal = nextVal * 3 + 6;
    console.log(nextVal);
    return nextVal;
  };
})();

getSomething();
getSomething();
getSomething();
getSomething();

再试着对它修改,将它改为一个标准的迭代器接口

var getSomething = (function () {
  var nextVal;
  return {
    [Symbol.iterator]: function () {
      return this;
    },
    next: function () {
      if (nextVal === undefined) nextVal = 1;
      else nextVal = nextVal * 3 + 6;
      return { done: nextVal > 500, value: nextVal };
    },
  };
})();

getSomething.next().value; // 1
getSomething.next().value; // 9
getSomething.next().value; // 33
getSomething.next().value; // 105

其中有些令人疑惑的代码大概就是[Symbol.iterable]: function() {return this} 这一行了

这一行的作用是将getSomething 的值也构建成为一个 iterable,现在它既是 iterable 也是迭代器

for (let v of getSomething) {
  console.log(v);
}
/* 
1
9
33
105
321
 */

5.2 生成器

可以把生成器看作一个值的生产者,我们通过迭代器接口的next 调用一次提取出一个值,所以严格来讲,生成器本身并不是 iterrable,尽管执行一个生成器就得到了一个迭代器

如果使用生成器来实现前面的getSometing :

function* getSomething() {
  var nextVal;
  while (true) {
    if (nextVal === undefined) nextVal = 1;
    else nextVal = nextVal * 3 + 6;
    yield nextVal;
  }
}

生成器会在每个yield 处暂停,函数getSomething 的作用域会被保持,也就意味着不需要闭包在调用之间保持变量状态

上面代码也同样可以使用for...of 循环

// 注意这里是getSomething(),得到了它的迭代器来进行循环的
for (var v of getSomething()) {
  console.log(v);
  // 不要死循环!
  if (v > 500) {
    break;
  }
}

但是上面的代码看起来getSomething 在break执行之后被永远挂起了。

不过并非如此,for...of 循环的“异常结束”,通常由 break,return 或者未捕获异常引起,会向生成器的迭代器发送一个信号使其终止。

来自:https://mp.weixin.qq.com/s/IdvvFfvgrwAt7DTJKnxGYA

链接: https://www.fly63.com/article/detial/10826

JavaScript异步编程:Generator与Async

Generator与async function都是返回一个特定类型的对象:Generator: 一个类似{ value: XXX, done: true }这样结构的Object,Async: 始终返回一个Promise,使用await或者.then()来获取返回值

es6中生成器Generator的使用场景

由于Generator可以暂停函数执行,返回任意表达式的值,这使得 Generator有多种应用场景,这篇文章简单整理一些Generator的使用场景:异步操作的同步化表达、控制流管理、部署 Iterator 接口、作为数据结构

Generator函数

Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

ES6 - Generator函数

Generator函数是ES6标准中提出的一种异步编程的解决方案。这种函数与普通函数最大的区别在于它可以暂停执行,又可以从暂停的位置恢复继续执行。从语法上看,Generator函数就是一个状态机,封装了许多内部状态。

es6 generator函数的异步编程

es6 generator函数,我们都知道asycn和await是generator函数的语法糖,那么genertaor怎么样才能实现asycn和await的功能呢?thunk函数 将函数替换成一个只接受回调函数作为参数的单参数函数

Generator函数的语法和应用

状态机,封装了多个内部状态;返回一个遍历器对象,通过改对象可以一次遍历Generator函数内部的每一个状态;带*号,yeild表达式定义不同的内部状态;调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果

Js Generators使用指南

最近,为了更好地理解Redux Sagas的工作原理,我重学了JavaScript generators的知识,我把从网上收集到的各种知识点浓缩到一篇文章里,我希望这篇文章既通俗易懂,又足够严谨,可以作为初学者的generators使用指南。

ES6 新特性之Generator

实际上Generator就是遍历器的一个生成器,我们可以用Generator来生成一个遍历器。Generator有两个明显的特点:第一个是function关键字与函数名之间有一个星号,一般而言是将两者写在一起的。第二个是在函数体内部有一个yield的关键字

如何实现Generator 执行器?

虽然现在使用 async 函数 就可以替代 Generator 执行器了,不过了解下 Generator 执行器的原理还是挺有必要的。如果你不了解 Generator,那么你需要看这里。

ES6深度解析:Generators

这是一只会说话的猫的一些代码,可能是当今互联网上最重要的一种应用。它看起来有点像一个函数,对吗?这被称为生成器-函数,它与函数有很多共同之处。但你马上就能看到两个不同之处。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!