Generator

Generator

1、什么是Generator

Generator函数是es6提供的一种异步编程解决方案,(promise也是…)

从语法上,可以把它理解为,Generator函数上一个状态机,内部封装了多个状态

Generator函数会返回一个iterator,也就是说,该函数还是一个iterator生成器,而生成的这个iterator会用于遍历内部的状态

形式上,Generator函数和上一个普通函数,有两个特征:

  • function 关 键字与函数名之间有一个星号
  • 函数体内部使用 yield 表达式,定义不同 的内部状态( yield 在英语里的意思就是“产出”)
function* helloWorldGenerator() {
    yield 'hello';
    yield 'world';
    return 'ending';
}
var hw = helloWorldGenerator();

2、yield表达式

由于 Generator 函数返回的遍历器对象,只有调用 next 方法才会遍历下一个内部 状态,所以其实提供了一种可以暂停执行的函数。 yield 表达式就是暂停标志。

遍历器对象的next方法的运行逻辑:

  1. 遇到yield表达式,就暂停后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回对象的value值
  2. 下次调用next方法,再继续往下执行,再次遇到yield表达式又停止执行,将表达式后面的值返回,以此类推
  3. 遇到return,返回return的值,否则返回undefined

yield后面的值,只有调用了next方法、内部指针指向该语句时,才会执行,因此等于为JavaScript提供了手动的惰性求值功能

如果没有yield表达式,generator函数就变成了暂缓执行函数,等于只有调用这个函数的next方法时,才会执行这个函数

注意:

  1. yield表达式,只能用于Generator函数

    function a() {
        yield 'xx'
    }
    a().next()
    // 报错
    
  2. 如果用于另一个表达式中,必须放在圆括号中

    let b = function*() {
      console.log('hello' + (yield 'xx'));
      return '1'
    }
    for (const iterator of b()) {
      console.log(iterator);
    }
    
  3. 如果是作为参数,或者放在赋值表达式右边,可以不加括号

    function* a() {
        const s = yield 'xx'
    }
    

数组扁平化:

let arr = [1, [
    [2, 3], 4
  ],
  [5, 6]
];
let newArr = []

function* flat(arr) {

  for (let i = 0; i < arr.length; i++) {
    if ((typeof arr[i]) !== 'number') {
      yield* flat(arr[i])

    } else {
      yield arr[i]
    }
  }
}

for (const f of flat(arr)) {
  newArr.push(f)
}
console.log(newArr);

3、与iterator接口的关系

由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象 的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口。

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]

而Generator函数执行后,返回一个遍历器对象,该对象具有Symbol.ierator属性,执行后返回自身

function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g
// true

4、next方法的参数

yield 表达式本身没有返回值,或者说总是返回 undefined 。 next 方法可以 带一个参数,该参数就会被当作上一个 yield 表达式的返回值。

这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下 文状态(context)是不变的。通过 next 方法的参数,就有办法在 Generator 函 数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运 行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。

例子:

function wrapper(generatorFun) {
  return function(...args) {
    let generator = generatorFun(...args)
    generator.next()
    return generator
  }

}


// let b = a()
// console.log(b.next('hello')); // 第一次无效
// console.log(b.next('hello'));

let b = wrapper(function* a(num) {
  console.log(`first input:${(yield)}`);
  return num
})
console.log(b(1).next('hello'));

注意,由于 next 方法的参数表示上一个 yield 表达式的返回值,所以在第一次 使用 next 方法时,传递参数是无效的。V8 引擎直接忽略第一次使用 next 方法 时的参数,只有从第二次使用 next 方法开始,参数才是有效的。从语义上讲,第 一个 next 方法用来启动遍历器对象,所以不用带有参数。

5、for…of…循环

for…of 循环可以自动遍历 Generator 函数时生成的 Iterator 对象,且此时 不再需要调用 next 方法。

for…of…循环遇到返回值对象属性为done时就停止,且不包含对应的值,也就是说该循环返回各个yield的值,但是不返回return的值

利用generator实现斐波那契数列

function* fibonacci() {
  let [pre, cur] = [0, 1]
  while (true) {
    [pre, cur] = [cur, pre + cur]
    yield cur
  }
}
for (const f of fibonacci()) {
  if (f > 1000) break;
  console.log(f);
}

使用generator给对象添加iterator,使之可以被forof遍历

function* objGenerator(obj) {
  let keys = Reflect.ownKeys(obj)
  for (const key of keys) {
    yield [key, obj[key]]
  }

}

let obj = {
  name: 'lsc',
  age: 18
}

for (const [key, value] of objGenerator(obj)) {
  console.log(`${key} is ${value}`);
}

像变量的赋值解构,数组的from方法,等遍历方法都是调用了iterator接口

6、generator函数返回遍历器对象的方法

6.1 Generator.prototype.throw

generator遍历器返回的函数对象,每个都有throw方法,该方法可以在gennerator函数体外抛出异常,在函数体内被捕获

var g = function* () {
    try {
        yield;
    } catch (e) {
        console.log('内部捕获', e);
    }
};
var i = g();
i.next();
try {
    i.throw('a');
    i.throw('b');
} catch (e) {
    console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b

第一次抛出异常内部捕获了之后就不再捕获了,因此第二个异常是外部捕获

注意:

  • 如果函数内部没有捕获异常,则被外面的捕获(如果外面有catch方法)
  • 如果函数内部,外部都没捕获的话,程序异常报错,中断执行
  • 如果异常被捕获,会执行下一个yield,等于会自动执行一次next
  • 全局的throw命令跟遍历器的throw方法是无关的,全局抛出的异常,遍历器不会去捕获
  • 函数体外抛出的异常可以被函数体内捕获,函数体内抛出的异常也可以被函数体外捕获,如果没有被内部捕获,则next会返回一个对象{value:undefined,done:true},他会认为执行已经结束

6.2 Generator.prototype.return

Generator 函数返回的遍历器对象,还有一个 return 方法,可以返回给定的值, 并且终结遍历 Generator 函数。

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

该方法可以传入参数,表示返回的值,如果不传,就返回undefined

如果函数中有try…finally语句块,则会等finally执行之后再return,终止这个函数

function* numbers () {
    yield 1;
    try {
        yield 2;
        yield 3;
    } finally {
        yield 4;
        yield 5;
    }
    yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }

7、next()、throw()、return() 的共同点

网友的理解三个方法本质上是同一件事,可以放在一起理解,它们的作用都是用不同的语句去替换yield表达式

  • next方法传入参数就相当于将表达式替换成那个值,否则就是undefined
  • throw方法相当于替换yield为传入的错误信息
  • return方法相当于将yield表达式替换为一个return

8、yield*表达式

在generator函数里面调用另一个generator函数是无效的,但是外面可以通过使用yield*表达式使之生效

function* foo() {
  yield 'x'
  yield* bar()
  yield 'y'
}
 
// 等同于
function* foo() {
  yield 'x'
  for (const f of bar()) {
    yield f

  }
  yield 'y'
}

// 等同于
function* foo() {
  yield 'x'
  yield 'a'
  yield 'b'
  yield 'y'
}
  1. yield*的含义就是识别后面跟的对象为一个遍历器对象
  2. yield*后面的 Generator 函数(没有 return 语句时),等同于在 Generator 函数内部,部署一个 for…of 循环。
  3. 如果有return语句,则需要用一个变量去接收该语句
  4. yield*可以遍历所有有iterator的结构,也就是yield* 可以替换for…of…,所以同理yield*后面可以跟数组,字符串等

使用yield*遍历二叉树

// 二叉树的构造函数 参数:左子树,当前节点,右子树
function Tree(left, label, right) {
  this.left = left
  this.label = label
  this.right = right
}


// 中序遍历函数
function* inorder(t) {
  if (t) {
    yield* inorder(t.left)
    yield t.label
    yield* inorder(t.right)
  }
}
// 创建二叉树
function make(array) {
  if (array.length == 1) return new Tree(null, array[0], null)
  return new Tree(make(array[0]), array[1], make(array[2]))
}

let tree = make([
  [
    ['a'], 'b', ['c']
  ], 'd', [
    ['e'], 'f', ['g']
  ]
])

let result = []
for (const f of inorder(tree)) {
  result.push(f)
}
console.log(result);

9、作为对象属性的generator函数

如果generator作为一个对象的属性,有两种写法

let obj = {
    *myGenerator() {
        
    }
}

let obj = {
    myGenerator: funtion* () {
        
    }
}

10、Generator函数的 this

Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的 prototype 对象上的方法。

但是如果把该函数当做普通的构造函数,并不会生效,因为该函数返回的总是遍历器对象,而不是this对象

Generator函数也不能跟new关键字一起使用,因为该函数不是一个构造函数

接下来对象他进行改造,使得我们既可以使用new关键字,又可以直接使用.运算符去拿值,且又有generator函数的特点

function* gen() {
    this.a = 1;
    yield this.b = 2;
    yield this.c = 3;
}
function F() {
    return gen.call(gen.prototype);
}
var f = new F();
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3

11、其他含义

Generator与状态机

Generator 是实现状态机的最佳结构。比如,下面的 clock 函数就是一个状态 机。

var ticking = true;
var clock = function() {
    if (ticking)
        console.log('Tick!');
    else
        console.log('Tock!');
    ticking = !ticking;
}

这个函数有两个状态,每运行一次改变一个状态,这就是状态机。

用generator函数实现:

var clock = function* () {
    while (true) {
        console.log('Tick!');
        yield;
        console.log('Tock!');
        yield;
    }
};

Generator与协程

协程(coroutine)是一种程序运行的方式,是比线程更轻量的单位,协程的定义参考另一篇笔记。协程在只有一个的情况下,相当于特殊的子例程,在多个的情况下就是我们正常理解的协程

(1)协程与子例程的差异

子例程:传统的例程是一个堆栈式后进先出的执行方式,也就是说最先进的必须执行完了才可以执行父函数。而协程可以暂停当前执行的函数(协程),去执行另一个协程(函数),当协程只有一个的时候,就不能暂停了,因此说单个协程是一个特殊的子例程。

由此我们可以看出,子例程只占有一个栈,协程占有多个栈,但只有一个栈在运行,也就是说协程是以牺牲内存来实现多任务的并行。

(2)协程与普通线程的差异

不难看出,协程适合用于多任务运行的环境。在这个意义上,它与普通的线程很相 似,都有自己的执行上下文、可以分享全局变量。它们的不同之处在于,同一时间 可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停 状态。此外,普通的线程是抢先式的,到底哪个线程优先得到资源,必须由运行环 境决定,但是协程是合作式的,执行权由协程自己分配。

由于 JavaScript 是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可 以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的 调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。

Generator 函数是 ES6 对协程的实现,但属于不完全实现。Generator 函数被称 为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序 的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的 协程继续执行。

如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用 yield 表示式交换控制权。

12、同步应用

(1)异步操作的同步化表达

如:封装了ajax的例子

function* main() {
  var result = yield request("http://39.103.170.255:8889/index/category/findAll");
  var resp = JSON.parse(result);
  console.log(resp);
}

function request(url) {
  makeAjaxCall(url, function(response) {
    it.next(response);
  });
}

function makeAjaxCall(url, fn) {
  const xhr = new XMLHttpRequest()
  xhr.open('GET', url)
    // xhr.responseType = 'json'
  xhr.setRequestHeader('Accept', 'application/json')
  xhr.send()

  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4) {
      if (xhr.status == 200) {
        fn(xhr.response)
      }
    }
  }
}

var it = main();
it.next();

(2)控制流管理

对于一个反复回调的函数

step1(function (value1) {
    step2(value1, function(value2) {
        step3(value2, function(value3) {
            step4(value3, function(value4) {
                // Do something with value4
            });
        });
    });
});

采用 Promise 改写上面的代码。

Promise.resolve(step1)
    .then(step2)
    .then(step3)
    .then(step4)
    .then(function (value4) {
    // Do something with value4
}, function (error) {
    // Handle any error from step1 through step4
})
    .done();

上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量 Promise 的语 法。Generator 函数可以进一步改善代码运行流程。

function* longRunningTask(value1) {
    try {
        var value2 = yield step1(value1);
        var value3 = yield step2(value2);
        var value4 = yield step3(value3);
        var value5 = yield step4(value4);
        // Do something with value4
    } catch (e) {
        // Handle any error from step1 through step4
    }
}

然后,使用一个函数,按次序自动执行所有步骤。

scheduler(longRunningTask(initialValue));
function scheduler(task) {
    var taskObj = task.next(task.value);
    // 如果Generator函数未结束,就继续调用
    if (!taskObj.done) {
        task.value = taskObj.value
        scheduler(task);
    }
}

下面,利用 for…of 循环会自动依次执行 yield 命令的特性,提供一种更一般 的控制流管理的方法。

let steps = [step1Func, step2Func, step3Func];
function *iterateSteps(steps){
for (var i=0; i< steps.length; i++){
var step = steps[i];
yield step();
}
}

(3)部署iterator接口

上面写到过,为对象添加iertator接口

(4)作为数据结构

可作为类似数组的数据结构进行遍历

13、异步应用

传统方法

  • 回调函数
  • 事件监听
  • 发布/订阅
  • promise对象

异步的概念

所谓"异步",简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成 两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二 段。 比如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要 求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务 的第二段(处理文件)。这种不连续的执行,就叫做异步。 相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作 系统从硬盘读取文件的这段时间,程序只能干等着。

回调函数

回调函数我们已经很熟悉了,要注意的点就是node中的回调函数

一个有趣的问题是,为什么 Node 约定,回调函数的第一个参数,必须是错误对 象 err (如果没有错误,该参数就是 null )?

原因是执行分成两段,第一段执行完以后,任务所在的上下文环境就已经结束了。 在这以后抛出的错误,原来的上下文环境已经无法捕捉,只能当作参数,传入第二 段。

Promise

为了解决回调函数的问题,提出了promise的写法,使用promise连续读取多个文件,写法如下:

var readFile = require('fs-readfile-promise');
readFile(fileA)
    .then(function (data) {
    console.log(data.toString());
})
    .then(function () {
    return readFile(fileB);
})
    .then(function (data) {
    console.log(data.toString());
})
    .catch(function (err) {
    console.log(err);
});

promsie虽然看似解决了回调地狱的问题,但也增加了很多冗余代码,且并无新意,更好的写法就是generator函数

Generator函数

generator函数是协程在es6的实现,最大的特点就是可以交给函数的执行权,即暂停执行

generator函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因,除此之外他还有两个特性,使他可以作为异步编程的完整解决方案:函数体外的数据交换错误处理机制

异步任务的封装

使用generator函数执行一个真实的异步任务

var fetch = require('node-fetch');
function* gen(){
    var url = 'https://api.github.com/users/github';
    var result = yield fetch(url);
    console.log(result.bio);
}

var g = gen();
var result = g.next();
result.value.then(function(data){
    return data.json();
}).then(function(data){
    g.next(data);
});

可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便 (即何时执行第一阶段、何时执行第二阶段)。

Thunk函数

Thunk 函数是自动执行 Generator 函数的一种方法。

了解:

  • 传名调用:直接传入表达式,要用到时再计算
  • 传值调用:先计算出来再传入

js的Thunk函数

在 JavaScript 语言 中,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, args)
    }
  }
}

// es6实现
let Thunk = function(fn) {
  return function(...args) {
    return function(callback) {
      return fn.call(this, ...args, callback)
    }
  }
}

function f(a, cb) {
  cb(a);
}
const ft = Thunk(f);
ft(1)(console.log)

Thunkify模块

生产环境的转换器,建议使用 Thunkify 模块。

npm install thunkify

基本使用

var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
    // ...
});

Generator函数的流程管理

有了thunk函数之后就可以结合thunk函数一起使用

let Thunk = function(fn) {
  return function(...args) {
    return function(callback) {
      return fn.call(this, ...args, callback)
    }
  }
}

function f(a, cb) {
  cb(a);
}
let ft = Thunk(f);

function* test() {
  let res1 = yield ft(2)
  console.log(res1);
  let res2 = yield ft(3)
  console.log(res2);
  return '4'
}
let g = test()
  // console.log(g.next());

let r1 = g.next()

r1.value(function(data) {
  let r2 = g.next(data)
  r2.value(function(data) {
    let re = g.next(data)
    console.log(re);
  })
})

f函数执行thunk函数之后返回的就是一个可以传入参数,并且返回值又是一个回调函数的函数了,在generator函数里面使用的话执行next之后返回值就是{value:function,done: x},因此可以在value的回调函数去决定是否要操作下一步

由于上面执行代码是重复的,因此可以使用递归来解决

function run(fn) {
  let g = fn()

  function next(data) {
    let res = g.next(data)
    if (res.done) return;
    res.value(next)
  }

  next()
}

function* g() {
  // ...
}

run(g)

co模块

co 模块是著名程序员 TJ Holowaychuk 于2013年6月发布的一个小工具,用于 Generator 函数的自动执行。

基本使用

var co = require('co');
co(gen).then(function (){
    console.log('Generator 函数执行完成');
});

引入co模块之后传入generator函数,该模块返回一个promise对象,因此可以直接then方法进行回调

co 模块的原理

前面说过,Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当 异步操作有了结果,能够自动交回执行权。

两种方式可以做到这一点

(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。 (上面的自动化流程管理就是这种方式)

(2)Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行 权。

co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个 模块。使用 co 的前提条件是,Generator 函数的 yield 命令后面,只能是 Thunk 函数或 Promise 对象。如果数组或对象的成员,全部都是 Promise 对象,也可以 使用 co,详见后文的例子。(co v4.0版以后, yield 命令后面只能是 Promise 对象,不再支持 Thunk 函数。)

基于promise对象的自动执行

// 要求yield表达式后面必须跟一个promise对象,而不是thunk函数

function ft(a, cb) { //原函数
  cb(a)
}

function ftPro(a) { // promise改造
  return new Promise((resolve, reject) => {
    ft(a, function(data) {
      resolve(data)
    })
  })
}

function* gen() {
  let r1 = yield ftPro(1)
  let r2 = yield ftPro(2)
  console.log(r1 + 'r1');
  console.log(r2 + 'r2');
}

let g = gen()

g.next().value.then(res => {
  console.log(res);
  g.next(res).value.then(res => {
    console.log(res);
    g.next(res)
  })
})

首先是手动执行,可以看到这里是把一个可以传入两个参数的ft函数(一个参数是数据,一个参数是回调函数),把这样一个函数改造成了返回promise的函数

思路是调用ftPro函数后返回一个promise对象,该对象resolve的data数据是ft函数回调函数之后处理过的数据(比如说可以用a,也可以是url去请求数据,然后将数据返回),因此在then方法中res就可以拿到数据了

下面封装自动执行

// 封装基于promise的自动化流程管理

function ft(a, cb) { //原函数
  cb(a)
}

function ftPro(a) { // promise改造
  return new Promise((resolve, reject) => {
    ft(a, function(data) {
      resolve(data)
    })
  })
}

function* gen() {
  let r1 = yield ftPro(1)
  let r2 = yield ftPro(2)
  console.log(r1 + 'r1');
  console.log(r2 + 'r2');
}

function run(fn) {
  let g = fn()

  function next(res) {
    let result = g.next(res)
    if (result.done) return result.value
    result.value.then(res => {
      console.log(res);
      next(res)
    })
  }

  next()

}

run(gen)

只是像上面一样加了封装

co模块的源码

是上面promise自动执行更严谨的实现

co模块支持并发的异步操作,即允许某些操作同时进行,等到它们全部完成,才进行下 一步。

例子:

co(function* () {
var values = [n1, n2, n3];
yield values.map(somethingAsync);
});
function* somethingAsync(x) {
// do something async
return y
}

上面的代码允许并发三个 somethingAsync 异步操作,等到它们全部完成,才会 进行下一步。

实例:处理 Stream

学习完node回来补充…

上一篇:Spring Boot集成MyBatis Generator插件自动生成代码(续)


下一篇:泛型接口的定义与使用