用 Generators做异步处理

生成器Generators简介

本文默认读者知道 generator是什么.

算了, 还是简单说一下吧~

function* 声明 (function关键字后跟一个星号)定义了一个生成器函数 (generator function),它返回一个 Generator 对象

generator function在内部有个 yield 关键字, 当 generator 对象执行 next 方法时, 会执行到下一个 yield 关键字所在的地方

function* genFn() {  
  let data = yield 1;
  console.log('data', data);
  let data2 = yield 2;
  console.log('data2', data2);
}

let genObj = genFn();

let firstYieldReturnValue = genObj.next(0);  
// 返回{ value: 1, done: false}

let secondYieldReturnValue = genObj.next(100);  
//输出'data 100', 返回{ value, 2, done: false}

let thirdYieldReturnValue = genObj.next(200);  
// 输出 'data2 200' 返回 {value, undifined: done: false}

从上面代码的执行结果看来, generator function会执行到 yield 处, 并把 yield 的后面的值, 传递到 generator 对象的 next().value 处, generator 对象执行 next(data)会把 next的实参传递到generator function 里对应的 yield关键字处, 这个特性非常关键, 我们将用这个特性来做异步处理, 让我们的代码看起来像同步的.

我们看到generator function是可以中断执行的

因为:

每当生成器执行yields语句,生成器的堆栈结构(本地变量、参数、临时值、生成器内部当前的执行位置)被移出堆栈。然而,生成器对象保留了对这个堆栈结构的引用(备份),所以稍后调用.next()可以重新激活堆栈结构并且继续执行.

让 generator 自动执行

for...of
function* genFn() {  
  let data = yield 1;
  console.log('data', data);
  let data2 = yield 2;
  console.log('data2', data2);
}
let g = genFn();  
for(let value of g) {  
}
//data undefined
//data2 undefined

这样是自动执行了, 不过没办法往 next 方法传参了.

while 循环:
let g = genFn();  
while(!g.next().done);  
//data undefined
//data2 undefined

这样写可不行, data 连yield后面的值都拿不到..

改进一下

let g = genFn();  
let next = g.next();  
while(!next.done) {  
  next = g.next(next.value)
}
// data 1
// data2 2

封装一下:

function run(g) {  
  let next = g.next();
  while(!next.done) {
    next = g.next(next.value)
  }
}
run(genFn());  
递归:
function run(g, value) {  
  let next = g.next(value);
  if(next.done) return next.value;
  return run(g, next.value);
}
run(genFn());  

处理异步

用 generator 处理异步函数, 让异步代码看起来像同步执行的;

这就需要像上面一样, 写一个能处理异步的自动执行器了.

promise

function p(data) {  
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(data);
    });
  });
}

function* genFn() {  
  let data = yield p(1);
  console.log(data);
}

p(1)返回的是一个 promise, 是异步的, 我们的期望是 data 得到的是 resolve 的值, 也就是1

我们试试用 while写这个自执行器:

function run(g) {  
  let next = g.next();
  while(!next.done) {
    next.value.then((data) => {
      g.next(data);
    })
  }
}

很明显这样是不行的.. 因为 while 一口气就给你执行完了, yield 还没拿到值呢...

所以我们要用递归

function run(g) {  
  function _run(data) {
      let result = g.next(data);
    if(result.done) return result.value;
    return result.value.then(_run);
  }
  return _run();
}
run(genFn());  

这样就比较开心了

解释一下, 因为 result.value 是一个 promise, 然后我们把_run放到 then 去执行, 就可以拿到resolve 的值, 然后一步一步 往下执行了. 美滋滋~

thunk 函数

什么是 thunk 函数?

其实就是对异步调用的函数做了一个简单的封装, 可以理解成对异步函数的颗粒化.

function asyncFn(options, callback) {  
  setTimeout(() => {
    callback(options);
  })
}
// 变成 thunk 函数以后
function asyncFn(option) {  
  return function(callback) {
    setTimeout(() => {
      callback(option);
    }))
  })
}

如果这还看不懂, 就去问问阮老师

yield 一个 thunk 函数
function asyncFn(option) {  
  return function(callback) {
    setTimeout(() => {
      callback(option);
    })
  }
}

function* genFn() {  
  let hello = yield asyncFn('hello world');
  console.log(hello);
}

// run 起来
function run(g) {  
  function _run(data) {
      let result = g.next(data);
    if(result.done) return result.value;
    return result.value(_run);
  }
  return _run();
}
run(genFn());  

这里因为 result.value 是一个函数, 我们将 callback传就去, 他就可以执行了.

再举个🌰

function query(sql) {  
  return function (cb) {
    mysql.query(sql, function(error, data) {
     if(error) throw error;
      cb(data);
    })
  }
}

run(function*() {  
  let girlFirend = yield query('select girlFirend from girls where face=beautiful');
  console.log(girlFirend);
});

其实 co大概就是这么个意思

最后

generator 的作用应该和他的语义(生成器)一样,当做一个生成器来用, generator 拿来做异步处理是一种 hack 手法, 如果条件允许的话, 还是用 async 函数吧,

( 我感觉用generator 还不如用 promise 呢

参考

MDN generators InfoQ 深入浅出 ES6 generator

-- Ming