深刻理解Generator

这篇文章旨在帮你真正了解Generator,文章较长,不过若是能花时间耐心看完,相信你已经可以彻底理解generator

为何要用generator

在前端开发过程当中咱们常常须要先请求后端的数据,再用拿来的数据进行使用网页页面渲染等操做,然而请求数据是一个异步操做,而咱们的页面渲染又是同步操做,这里ES6中的generator就能发挥它的做用,使用它能够像写同步代码同样写异步代码。下面是一个例子,先忽略下面的写法,后面会详细说明。若是你已经理解generator基础能够直接跳过这部分和语法部分,直接看深刻理解的部分。javascript

function *foo() {
  // 请求数据
  var data = yield makeAjax('http://www.example.com');
  render(data);
}

在等待数据的过程当中会继续执行其余部分的代码,直到数据返回才会继续执行foo中后面的代码,这是怎么实现的那?咱们都知道js是单线程的,就是说咱们不可能同时执行两段代码,要实现这种效果,咱们先来猜测下,咱们来假设有一个“王杖”(指代cpu的执行权),谁拿到这个“王杖”,谁就能够作本身想作的事,如今代码执行到foo咱们如今拿着“王杖”而后向服务器请求数据,如今数据尚未返回,咱们不能干等着。做为王咱们有着高尚的马克思主义思想,咱们先把本身的权利交出去,让下一个须要用的人先用着,固然前提是要他们约定好一下子有须要,再把“王杖”还给咱们。等数据返回以后,咱们再把咱们的“王杖”要回来,就能够继续作咱们想作的事情了。
若是你理解了这个过程,那么恭喜你,你已经基本理解了generator的运行机制,我这么比喻虽然有些过程不是很贴切,但基本是这么个思路。更多的东西仍是向下看吧。前端

generator语法

generator函数

在用generator以前,咱们首先要了解它的语法。在上面也看到过,它跟函数声明很像,但后面有多了个*号,就是function *foo() { },固然也能够这么写function* foo() { }。这里两种写法没有任何区别,全看我的习惯,这篇文章里我会用第一种语法。如今咱们按这种语法声明一个generator函数,供后面使用。java

function *foo() {

}

yield

到目前为止,咱们还什么也干不了,由于咱们还缺乏了一个重要的老伙计yieldyield翻译成汉语是产生的意思。yield会让咱们跟在后面的表达式执行,而后交出本身的控制权,停在这里,直到咱们调用next()才会继续向下执行。这里新出现了next咱们先跳过,先说说generator怎么执行。先看一个例子。git

function *foo() {
  var a = yield 1 + 1;
  var b = yield 2 + a;
  console.log(b);
}

var it = foo();
it.next();
it.next(2);
it.next(4);

下面咱们来逐步分析,首先咱们定义了一个generator函数foo,而后咱们执行它foo(),这里跟普通函数不一样的是,它执行完以后返回的是一个迭代器,等着咱们本身却调用一个又一个的yield。怎么调用那,这就用到咱们前面提到的next了,它可以让迭代器一个一个的执行。好,如今咱们调用第一个it.next(),函数会从头开始执行,而后执行到了第一个yield,它首先计算了1 + 1,嗯,而后停了下来。而后咱们调用第二个it.next(2),注意我这里传入了一个2做为next函数的参数,这个2传给了a做为它的值,你可能还有不少其余的疑问,咱们详细的后面再说。接着来,咱们的it.next(2)执行到了第二个yield,并计算了2 + a因为a2因此就变成了2 + 2。第三步咱们再调用it.next(4),过程跟上一步相同,咱们把b赋值为4继续向下执行,执行到了最后打印出咱们的b4。这就是generator执行的所有的过程了。如今弄明白了yieldnext的做用,回到刚才的问题,你可能要问,为何要在next中传入24,这里是为了方便理解,我手动计算了1 + 12 + 2的值,那么程序本身计算的值在哪里?是next函数的返回值吗,带着这个疑问,咱们来看下面一部分。es6

next

next的参数

next能够传入一个参数,来做为上一次yield的表达式的返回值,就像咱们上面说的it.next(2)会让a等于2。固然第一次执行next也能够传入一个参数,但因为它没有上一次yield因此没有任何东西可以接受它,会被忽略掉,因此没有什么意义。github

next的返回值

在这部分咱们说说next返回值,废话很少说,咱们先打印出来,看看它究竟是什么,你能够本身执行一下,也能够直接看我执行的结果。后端

function *foo() {
  var a = yield 1 + 1;
  var b = yield 2 + a;
  console.log(b);
}

var it = foo();
console.log(it.next());
console.log(it.next(2));
console.log(it.next(4));

执行结果:api

{ value: 2, done: false }
{ value: 4, done: false }
4
{ value: undefined, done: true }

看到这里你会发现,yield后面的表达式执行的结果确实返回了,不过是在返回值的value字段中,那还有done字段使用来作什么用的那。其实这里的done是用来指示咱们的迭代器,就是例子中的it是否执行完了,仔细观察你会发现最后一个it.next(4)返回值是done: true的,前面的都是false,那么最后一个打印值的undefined又是什么那,由于咱们后面没有yield了,因此这里没有被计算出值,那么怎么让最后一个有值那,很简单加个return。咱们改写下上面的例子。数组

function *foo() {
  var a = yield 1 + 1;
  var b = yield 2 + a;
  return b + 1;
}

var it = foo();
console.log(it.next());
console.log(it.next(2));
console.log(it.next(4));

执行结果:promise

{ value: 2, done: false }
{ value: 4, done: false }
{ value: 5, done: true }

最后的nextvalue的值就是最终return返回的值。到这里咱们就再也不须要手动计算咱们的值了,咱们在改写下咱们的例子。

function *foo() {
  var a = yield 1 + 1;
  var b = yield 2 + a;
  return b + 1;
}

var it = foo();
var value1 = it.next().value;
var value2 = it.next(value1).value;
console.log(it.next(value2));

大功告成!这些基本上就完成了generator的基础部分。可是还有更多深刻的东西须要咱们进一步挖掘,看下去,相信你会有收获的。

深刻理解

前两部分咱们学习了为何要用generator以及generator的语法,这些都是基础,下面咱们来看点不同的东西,老规矩先带着问题才能更有目的性的看,这里先提出几个问题:

  • 怎样在异步代码中使用,上面的例子都是同步的啊
  • 若是出现错误要怎么进行错误的处理
  • 一个个调用next太麻烦了,能不能循环执行或者自动执行那

迭代器

进行下面全部的部分以前咱们先说一说迭代器,看到如今,咱们都知道generator函数执行完返回的是一个迭代器。在ES6中一样提供了一种新的迭代方式for...offor...of能够帮助咱们直接迭代出每一个的值,在数组中它像这样。

for (var i of ['a', 'b', 'c']) {
  console.log(i);
}

// 输出结果
// a
// b
// c

下面咱们用咱们的generator迭代器试试

function *foo() {
  yield 1;
  yield 2;
  yield 3;
  return 4;
}

// 获取迭代器
var it = foo();

for(var i of it) {
  console.log(i);
}

// 输出结果
// 1
// 2
// 3

如今咱们发现for...of会直接取出咱们每一次计算返回的值,直到done: true。这里注意,咱们的4没有打印出来,说明for...of迭代,是不包括donetrue的时候的值的。

下面咱们提一个新的问题,若是在generator中执行generator会怎么样?这里咱们先认识一个新的语法yield *,这个语法可让咱们在yield跟一个generator执行器,当yield遇到一个新的generator须要执行,它会先将这个新的generator执行完,再继续执行咱们当前的generator。这样说可能不太好理解,咱们看代码。

function *foo() {
  yield 2;
  yield 3;
  yield 4;
}

function * bar() {
  yield 1;
  yield *foo();
  yield 5;
}

for ( var v of bar()) {
  console.log(v);
}

这里有两个generator咱们在bar中执行了foo,咱们使用了yield *来执行foo,这里的执行顺序会是yield 1,而后遇到foo进入foo中,继续执行foo中的yield 2直到foo执行完毕。而后继续回到bar中执行yield 5因此最后的执行结果是:

1
2
3
4
5

异步请求

咱们上面的例子一直都是同步的,但实际上咱们的应用是在异步中,咱们如今来看看异步中怎么应用。

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

function *foo() {
  var data = yield request('http://api.example.com');
  console.log(JSON.parse(data));
}

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

这里又回到一开头说的那个例子,异步请求在执行到yield的时候交出控制权,而后等数据回调成功后在回调中交回控制权。因此像同步同样写异步代码并非说真的变同步了,只是异步回调的过程被封装了,从外面看不到而已。

错误处理

咱们都知道在js中咱们使用try...catch来处理错误,在generator中相似,若是在generator内发生错误,若是内部能处理,就在内部处理,不能处理就继续向外冒泡,直到可以处理错误或最后一层。

内部处理错误:

// 内部处理
function *foo() {
  try {
    yield Number(4).toUpperCase();
  } catch(e) {
    console.log('error in');
  }
}

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

// 运行结果:error in

外部处理错误:

// 外部处理
function *foo() {
  yield Number(4).toUpperCase();
}

var it = foo();
try {
  it.next();
} catch(e) {
  console.log('error out');
}

// 运行结果:error out

generator的错误处理中还有一个特殊的地方,它的迭代器有一个throw方法,可以将错误丢回generator中,在它暂停的地方报错,再日后就跟上面同样了,若是内部能处理则内部处理,不能内部处理则继续冒泡。

内部处理结果:

function *foo() {
  try {
    yield 1;
  } catch(e) {
    console.log('error', e);
  }
  yield 2;
  yield 3;
}

var it = foo();
it.next();
it.throw('oh no!');

// 运行结果:error oh no!

外部处理结果:

function *foo() {
  yield 1;
  yield 2;
  yield 3;
}

var it = foo();
it.next();
try {
  it.throw('oh no!');
} catch (e) {
  console.log('error', e);
}

// 运行结果:error oh no!

根据测试,发现迭代器的throw也算做一次迭代,测试代码以下:

function *foo() {
  try {
    yield 1;
    yield 2;
  } catch (e) {
    console.log('error', e);
  }
  yield 3;
}

var it = foo();
console.log(it.next());
it.throw('oh no!');
console.log(it.next());

// 运行结果
// { value: 1, done: false }
// error oh no!
// { value: undefined, done: true }

当用throw丢回错误的时候,除了try中的语句,迭代器迭代掉了yield 3下次再迭代就是,就是最后结束的值了。错误处理到这里就没有了,就这么点东西^_^。

自动运行

generator能不能自动运行?固然能,而且有不少这样的库,这里咱们先本身实现一个简单的。

function run(g) {
  var it = g();

  // 利用递归进行迭代
  (function iterator(val) {
    var ret = it.next(val);

    // 若是没有结束
    if(!ret.done) {
      // 判断promise
      if(typeof ret.value === 'object' && 'then' in ret.value) {
        ret.value.then(iterator);
      } else {
        iterator(ret.value);
      }
    }
  })();
}

这样咱们就能自动处理运行咱们的generator了,固然咱们这个很简单,没有任何错误处理,如何让多个generator同时运行,这其中涉及到如何进行控制权的转换问题。我写了一个简单的执行器Fo,其中包含了Kyle Simpson大神的一个ping-pong的例子,感兴趣的能够看下这里是传送门,固然能顺手star一下就更好了,see you next article ~O(∩_∩)O~。

参考连接

相关文章
相关标签/搜索