ES6 异步进阶第二步:Generator 函数

一、什么是生成器 Generator?

生成器对象是由一个 Generator 函数返回的,并且她符合 可迭代协议和迭代器协议

语法:
function * gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen(); // "Generator { }"

生成器函数有以下四个特征:

  • 在 function 关键字和函数名之间多了一个星号
  • 函数内部使用了 yield 表达式,用于定义 Generator 函数中的每个状态
  • Generator 函数通过多个 yield 表达式定义内部状态,掉用 Generator 函数时,不会像普通函数会立即执行,而是返回的是一个 Iterator 对象,通过调用 next() 方法,可以依次遍历 Generator 函数的每个内部状态
  • Generator 函数的星号的位置有四种情况 如下
function *gen () {}   
function* gen () {}
function * gen () {}
function*gen () {}

一般的写法是第二种。

二、如何使用 Generator?

1、yield 表达式

yield 关键字在 Generator 函数中有两个作用:定义内部状态和暂停执行,代码解释如下:

function *gen () {
  yield 1
  yield 2
  return 3
}

const g = gen()   // Iterator对象
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: true}
g.next() // {value: undefined, done: true}

结合上述代码和第一节的Generator 函数的特征可知:
在调用 gen 函数时,返回的一个 Iterator 对象,要想获得每个 yield 表达式的状态,需要调用 next 方法。
每次调用 next 方法时,都会返回一个包含 value 属性和 done 属性的对象,value 属性表示 yield 表达式的值,done 属性是一个布尔值,表示遍历是否结束。
如果 value 没有返回值或者 返回的是 undefined ,说明是函数运行结束了。
还有一种情况需要注意的额是:yield 表达式 如果用在另一个表达式中,需要为其加上圆括号“()”,作为函数参数和语句时可以不适用圆括号

function *gen () {
  console.log('hello' + yield) ×
  console.log('hello' + (yield)) √
  console.log('hello' + yield '凯斯') ×
  console.log('hello' + (yield '凯斯')) √
  foo(yield 1)  √
  const param = yield 2  √
}
2、yield* 表达式
yield* 表达式的使用场景:在一个 Generator 函数中调用另一个 Generator 函数。下面代码是不能执行的:
function *foo () {
  yield 1
}
function *gen () {
  foo()
  yield 2
}
const g = gen()
g.next()  // {value: 2, done: false}
g.next()  // {value: undefined, done: true}

如果使用 yield 表达式,value 返回的值是一个遍历器对象:

function *foo () {
  yield 1
}
function *gen () {
  yield foo()
  yield 2
}
const g = gen()
g.next()   // {value: Generator, done: false}
g.next()   // {value: 2, done: false}
g.next()   // {value: undefined, done: true}

这里要实现上面场景,要使用 yield* 表达式,其实这个表达式就是 for...of 方法的简写:

function *foo () {
  yield 1
}
function *gen () {
  yield* foo()
  yield 2
}
const g = gen()
g.next()   // {value: 1, done: false}
g.next()   // {value: 2, done: false}
g.next()   // {value: undefined, done: true}

// 相当于
function *gen () {
  yield 1
  yield 2
}

// 相当于
function *gen () {
  for (let item of foo()) {
    yield item
  }
  yield 2
}

yield* 遍历具有 Iterator 接口的数据类型:

const arr = ['a', 'b']
const str = 'yuan'
function *gen () {
  yield arr
  yield* arr
  yield str
  yield* str
}
const g = gen()
g.next() // {value: ['a', 'b'], done: false}
g.next() // {value: 'a', done: false}
g.next() // {value: 'b', done: false}
g.next() // {value: 'yuan', done: false}
g.next() // {value: 'y', done: false}
...

使用 yield* 表达式取出嵌套数组中的成员:

// 普通方法
const arr = [1, [[2, 3], 4]]
const str = arr.toString().replace(/,/g, '')
for (let item of str) {
  console.log(+item)      // 1, 2, 3, 4
}

// 使用yield*表达式
function *gen (arr) {
  if (Array.isArray(arr)) {
    for (let i = 0; i < arr.length; i++) {
      yield * gen(arr[i])
    }
  } else {
    yield arr
  }
}
const g = gen([1, [[2, 3], 4]])
for (let item of g) {
  console.log(item)       // 1, 2, 3, 4
}
3、next 方法

next 方法的作用是分阶段执行 Generator 函数。
每一次调用next方法,就会从函数头部或者上一次停下来的地方开始执行,直到遇到下一个yield表达式(return 语句)为止。同时,调用next方法时,会返回包含value和done属性的对象,value属性值可以为yield表达式、return语句后面的值或者undefined值,done属性表示遍历是否结束。
遍历器对象 next 方法的执行逻辑如下:

  1. 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield表达式后面的那个表达式的值,作为返回的对象的value属性值。
  2. 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
  3. 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到遇到return语句为止,并将return语句后面表达式的值,作为返回的对象的value属性值。
  4. 如果该函数没有return语句,则返回的对象的value属性值为undefined。
function *gen () {
  yield 1
  yield 2
  return 3
}

const g = gen()
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: true}
g.next() // {value: undefined, done: true}

根据next运行逻辑再针对这个例子,就很容易理解了。调用gen函数,返回遍历器对象。
第一次调用next方法时,在遇到第一个yield表达式时停止执行,value属性值为1,即yield表达式后面的值,done为false表示遍历没有结束;
第二次调用next方法时,从暂停的yield表达式后开始执行,直到遇到下一个yield表达式后暂停执行,value属性值为2,done为false;
第三次调用next方法时,从上一次暂停的yield表达式后开始执行,由于后面没有yield表达式了,所以遇到return语句时函数执行结束,value属性值为return语句后面的值,done属性值为true表示已经遍历完毕了。
第四次调用next方法时,value属性值就是undefined了,此时done属性为true表示遍历完毕。以后再调用next方法都会是这两个值。

4、next 方法的参数

yield 语句本身没有返回值,或者说总是返回 undefined。

function *gen () {
  var x = yield 'hello world'
  var y = x / 2
  return [x, y]
}
const g = gen()
g.next()    // {value: 'hello world', done: false}
g.next()    // {value: [undefined, NaN], done: true}

由上,第二次调用 next 方法时,并没有返回相应的值,而是返回 undefined,解决方法:给第二次调用的 next 方法传入一个参数,该参数会当作上一个 yield 语句的返回值

function *gen () {
  var x = yield 'hello world'
  var y = x / 2
  return [x, y]
}
const g = gen()
g.next()    // {value: 'hello world', done: false}
g.next(10)    // {value: [10, 5], done: true}```

注意,这里的 next 方法的参数是指上一个 yield 语句的返回值,所以在第一次调用 next 不能传入参数。如若要传参数,需要借用闭包来实现。

function wrapper (gen) {
  return function (...args) {
    const genObj = gen(...args)
    genObj.next()
    return genObj
  }
}
const generator = wrapper(function *generator () {
  console.log(`hello ${yield}`)
  return 'done'
})
const a = generator().next('keith')
console.log(a)   // hello keith, done

实际上,yield 表达式和 next 方法是 generator 函数的双向信息传递。yield 表达式向外传递 value 值,next 方法的参数向内传递值。

5、遍历 Generator 对象
与 Iterator 接口的关系

任何一个对象的Symbol.iterator属性,指向默认的遍历器对象生成函数。而Generator函数也是遍历器对象生成函数,所以可以将Generator函数赋值给Symbol.iterator属性,这样就使对象具有了Iterator接口。默认情况下,对象是没有Iterator接口的。
具有Iterator接口的对象,就可以被扩展运算符(...),解构赋值,Array.from和for...of循环遍历了。

function *gen () {
  yield 1
  yield 2
  yield 3
  return 4
}
for (let item of gen()) {
  console.log(item)  // 1 2 3
}

for...of 循环可以自动遍历Generator函数生成的Iterator对象,不用调用next方法。

三、应用实例

1、协程

所谓协程:是指多个线程相互协作,完成异步任务。
Generator 函数是协程在 ES6 中的实现,最大特点就是可以交出函数的执行权。
整个Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方都用 yield 语句注明。Generator 函数实现协程代码如下:

function* gen() {
  var y = yield x + 2;
  return y;
}
var g = gen(1);
g.next(); // { value: 3, done: false };
g.next(); // { value: undefined; done: true }

2、Thunk 函数

Thunk 函数是自动执行 Generator 函数的一种方法。
Thunk 函数的含义:起源于“传名调用”的“求职策略”。将参数放到一个临时函数之中,在将这个临时参数传入函数体。这个临时函数就是 Thunk 函数。
在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
任何函数,只要参数有回调函数,都能写成 Thenk 函数的形式。
Thunk 函数转换器:

// ES5 版本
var Thunk = function(fn) {
  return function () {
    var args = Array.prototype.slice.call(arguments);
    return function (callback) {
      args.push(callback);
      return fn.apply(this, arg);
    }
  }
};

// ES6 版本
const Thunk = function(fn) {
  return function(...args) {
  return function(callback) {
      return fn.call(this, ...args, callback);
    }
  }
}

使用上面的转换器,生成, fs.redFile 的 Thunk 函数。

var readFileThunk = Thunk(fs.readFile);
redadFileThunk(fileA)(callback);`

Thunk 函数真正的威力在于可以自动执行 Generator 函数。

function run(fn) {
  var gen = fn();
  function next(err, data) {
    var result = gen.next(data);
    if(result.done) return;
    result.value(next);
  }
  next();
}
function* g() {
  ...
}

run(g);

上面是一个基于 Thunk 函数的 Generator 执行器,可以在generator 函数内部执行多个异步操作。但是,这个里的每一个异步操作都必须是 Thunk 函数,也就是说,跟在 yield 表达式后面的必须是 Thunk 函数。

3、Co 模块

co 模块是用于 Generator 函数自动执行的一个小工具。
co 模块是讲两种自动执行器(Thunk 函数和 Promise 对象)包装成一个模块。使用 co 模块的前提条件是,Generator 函数的 yield 名利后面只是 Thunk 函数或 Promise 对象。
使用 co 模块处理并发的异步操作 :
1、并发操作放在数组里

co(function* () {
  var res = yield [
    Promise.resolve(1),
    Promise.resolve(2)
  ];
  console.log(res);
}).catch(onerror);

2、并发操作写在对象里

co(function* () {
  var res = yield {
    1: Promise.resolve(1),
    2: Promise.resolve(2)
  };
  console.log(res);
}).catch(onerror);

3、处理多个 generator 异步操作

co(function* () {
  var valuse = [n1, n2, n3];
  yield values.map(somethingAsync);
});

function* somethingAsync(x) {
  // do something async
  return y;
}

上面代码允许并发 3 个 somethingAsync 异步操作。

四、结语

本章需要了解的是生成器函数 Generator 的具体含义及作用,如何使用 Generator 函数,以及 Generator 函数的几个特殊的应用实例。

戳我博客

章节目录

1、ES6中啥是块级作用域?运用在哪些地方?
2、ES6中使用解构赋值能带给我们什么?
3、ES6字符串扩展增加了哪些?
4、ES6对正则做了哪些扩展?
5、ES6数值多了哪些扩展?
6、ES6函数扩展(箭头函数)
7、ES6 数组给我们带来哪些操作便利?
8、ES6 对象扩展
9、Symbol 数据类型在 ES6 中起什么作用?
10、Map 和 Set 两数据结构在ES6的作用
11、ES6 中的Proxy 和 Reflect 到底是什么鬼?
12、从 Promise 开始踏入异步操作之旅
13、ES6 迭代器(Iterator)和 for...of循环使用方法
14、ES6 异步进阶第二步:Generator 函数
15、JavaScript 异步操作进阶第三步:async 函数
16、ES6 构造函数语法糖:class 类

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

推荐阅读更多精彩内容

  • 异步编程对JavaScript语言太重要。Javascript语言的执行环境是“单线程”的,如果没有异步编程,根本...
    呼呼哥阅读 7,301评论 5 22
  • 在此处先列下本篇文章的主要内容 简介 next方法的参数 for...of循环 Generator.prototy...
    醉生夢死阅读 1,439评论 3 8
  • 简介 基本概念 Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。本章详细介绍...
    呼呼哥阅读 1,070评论 0 4
  • 笔记,总结摘录自阮一峰笔记中有不少自己看书的总结 基本概念 核心目的:异步编程解决方案 关键概念:状态机,执行权限...
    布蕾布蕾阅读 5,103评论 0 4
  • 今天在群里看见一个引导页的效果,感觉好不错,于是就撸了下,先看看效果图 我们可以看到,当我们滑动页面的时候,会有一...
    ReturnYHH阅读 2,036评论 0 3