Generator
Generator 函数是 ES6 提供的一种异步编程解决方案,可以把它理解成一个状态机,封装了多个内部状态,执行 Generator 函数会依次遍历 Generator 函数内部的每一个状态。
传统函数
//普通函数——一路到底
function sayHello() {
alert('hello');
alert('world');
}
sayHello();
这个函数执行后会依次弹出 "hello" 和 "world",那么,我们能不能让他在弹出 hello 之后停下呢, Generator 函数就能满足我们的需求。
一、概念
学习 Generator ,我们需要了解下面三个基本概念:
-
function*
用来声明一个函数是生成器函数,它比普通的函数声明多了一个星号 *。, ES6 中 没有规定,function关键字与函数名之间的星号应该写在哪个位置。
-
yield
定义不同的内部状态,函数遇到 yield 的时候会暂停,并把 yield 后面的表达式结果抛出去,注意 yield只能在 Generator 函数中出现。
-
next
作用是恢复函数执行,调用next方法,使得指针移向下一个状态,直到遇到下一个 yield 表达式(或 return 语句)为止。
二、基本使用
function* sayHello() {
yield 'hello';
yield 'world';
return 'ending';
}
let sy = sayHello();
//依次执行
sy.next()
// { value: 'hello', done: false }
sy.next()
// { value: 'world', done: false }
sy.next()
// { value: 'ending', done: true }
sy.next()
// { value: undefined, done: true }
上面代码一共调用了四次 next 方法。
第一次调用,Generator 函数开始执行,遇到第一个 yield 表达式停下来,并且返回一个对象,它的 value 属性就是当前 yield 表达式的值"hello",done 属性的值 false,表示遍历还没有结束。
第二次调用,Generator 函数从上次 yield 表达式停下的地方,一直执行到下一个 yield 表达式。next 方法返回的对象的value属性为 yield 当前表达式的值"world",done 属性的值 false,表示遍历还没有结束。
第三次调用,Generator 函数从上次yield表达式停下的地方,一直执行到 return 语句。next 方法返回的对象的 value 属性,为 return 语句后面的表达式的值,done 属性的值 true,表示遍历已经结束。
第四次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。
yield 与 return
不难发现 ,yield 与 return既有相同点,也有区别。 相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到 yield,函数暂停执行,下一次再从该位置继续向后执行,而执行完 return 后函数也就结束了。 一个函数里面,只能执行一个 return 语句,但是可以执行多个 yield 表达式。正常函数只能返回一个值,因为只能执行一次 return;Generator 函数可以返回一系列的值,因为可以有任意多个yield。
next 的参数
yield 表达式本身没有返回值,或者说总是返回 undefined。 next 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。
看一个例子
function* foo(x) {
let y = 2 * (yield (x + 1));
let z = yield (y / 3);
return (x + y + z);
}
let a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
let b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
上面代码中,第一次运行 next 方法时,函数暂停于 yield (x + 1)
处,返回 value 为6
, 与第二次运行 next 方法的时候不带参数,导致 y 的值等于2 * undefined
,即NaN
,执行后面表达式都等于 NaN
。第三次运行 next 方法的时候不带参数,所以 z 等于 undefined
,返回对象的 value 等于5 + NaN + undefined
,即NaN。
如果向 next 方法提供参数,返回结果就完全不一样了。上面代码第一次调用 b 的 next 方法时,返回x+1
的值6;第二次调用 next 方法,将上一次 yield 表达式的值设为12
,因此 y 等于24
,返回y / 3
的值8
;第三次调用 next 方法,将上一次 yield 表达式的值设为13
,因此z等于13
,这时x等于5
,y 等于24
,所以 return 语句的值等于42
。
注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
三、Generator 函数的异步应用
所谓"异步",简单说就是一个任务不是连续完成的,任务被分成多段先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。处理异步常见的方法有:
回调函数(callback)
简单理解为,把任务放在不同函数里边,依次将后面的函数作为参数传入前一个函数中并执行,如下
function a(data, b(1));
function b(data, c(2));
function c(data, ...);
// ...
不难想象,这样的代码出现多重嵌套,很快就会乱成一团,无法管理。因为多个异步操作形成了强耦合,只要有一个操作需要修改,它的上层回调函数和下层回调函数,可能都要跟着修改,于是就出现了为"回调函数地狱"(callback hell)。
Promise
形式上“解决”回调函数地狱问题。
new Promise().then(function (data) {
console.log(data);
})
.then(function (data) {
// ...
})
.then(function (data) {
// ...
})
.catch(function (err) {
// ...
});
可以看到,Promise 对象只是一种新的写法,将回调函数的嵌套改为链式调用使用then方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。
Generator 函数
Generator 函数中有个协程的概念,即多个进程协作,协程是碰到 yield 把操作权交给 yield 的另一个进程,然后等它执行完了,把执行权交回来。 协程有点像函数,又有点像线程。它的运行流程大致如下。
function* funA() {
// ...
let f = yield funB();
// ...
}
- 第一步,协程A开始执行。
- 第二步,协程A执行到一半,进入暂停,执行权转移到协程B。
- 第三步,执行某些操作后,协程B交还执行权。
- 第四步,协程A恢复执行。
下面看看如何使用 Generator 函数,执行一个真实的异步任务。
import Ajax from 'ajax'; //引入ajax请求库
function* getData(){
let url = '/api/test';
let result = yield Ajax.get(url);
console.log(result.data);
}
上面代码中,Generator 函数封装了一个异步操作,该操作先读取一个接口,然后操作返回的数据。
接下来调用这个方法。
let g = getData();
let result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
上面代码中,首先执行 Generator 函数,获取遍历器对象,然后使用 next 方法(第二行),执行异步任务的第一阶段。如果 Ajax 返回的是一个 Promise 对象,我们可以用then方法调用下一个 next 方法。