概述
这是ES6提供的一种异步编程解决方案,异步的实现就是靠这方案实现的!
这函数可以理解成一个状态机,封装了许多内部状态,并且返回一个遍历器对象,可以依次遍历Generator函数内部的每一个状态。
这个函数与平常写的函数有些不同:
function
关键字与函数名之间有一个星号。- 函数体内部使用
yield
表达式,用来定义不同的内部状态。
以下定义一个名为 helloWorldGenerator
,内部有两个 yield
表达式,表示三状态:hello、world和return语句 (结束执行)
function* helloWorldGenerator(){
yield 'hello';
yield 'world';
return 'ending';
}
同时,这个函数调用后并不会执行,返回的也不是该函数的执行结果,而是一个内部状态的指针对象,也就是遍历器对象(Iteratro Object)。
如果一个对象的属性是Generator函数,可以简写如下:
let obj = {
//简写前:
myGeneratorMethod: function* (){ //some code }
//简写后:
*myGeneratorMethod(){ //some code }
}
yield 表达式
定义: 有于Generator函数需要调用next
方法才会遍历下一个内部状态,所以会提供一个可以暂停执行的函数,yield
就是这样的一个标志。
换句话说,执行一次Generator函数后,指针就会指向当前下一个状态(即yield
后面紧跟着的表达式的值),并停留在那个状态。
到下一次调用next
方法后,也会继续往下执行,直到遇到下一个yield
表达式。如果到最后没有yield
表达式,就一直到函数结束直到return
为止,并将后面的表达式的值,作为返回的对象的value
属性值。要是连return
都没有,返回的value
属性就会是undefined
。
注意:yield
只能用在Generator函数里,用在其他地方会报错!即使在Generator函数里包含一个普通函数,而这个普通函数里面用yield
也会报错。
Iterator接口关系
如何使一个对象拥有Iterator接口?如下:
let myIterable = {}
myIterable[Symbol.iterator] = function* (){
yield 1;
yield 2;
yield 3;
}
[...myIterable] //[1, 2, 3]
上面代码中,Generator函数赋给对象的Symbol.iterator
属性,使得myIterable
对象具有了Iterator接口,可以对这个对象使用扩展运算符了。
Generator函数执行后,返回一个遍历器对象,该对象本身也具有Symbol.iterator
属性,执行后返回自身。如下代码:
function* gen(){
//some code
}
let g = gen();
g[Symbol.iterator]() === g //true
next 方法的参数
其实yiedl
本身是不会有返回值的,但是,如果next
带一个参数的话,该参数就会被当作上一个yield
表达式的返回值。
一般函数一旦执行了就会直到结束,而因为Generator函数有暂停机制,即每次都要执行next
方法才会继续执行到下一个yield
表达式。
这时,可以利用next
方法的参数,来调整下一次的运行。
例如:
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() //Object{ value: 6, done: false}
b.next(12) //Object{ value: 8, done: false}
b.next(13) //Object{ value: 42, done: true}
注意:第一次调用next
传参无效,按语议来说第一次调用next
是用来启动遍历器对象,所以不用带参。
以上代码可一看出,第一次传参是有效的,但第二次没有传参,导致第2行的运行结果是let y = 2 * (yield (undefined + 1))
,y的值就成了NaN
!
所以从12行开始的代码传参后没有报错。
for…of 循环
如果说next
指向下一个状态并停留在那个状态的话,那么for...of
则是一次性执行完所有状态。如:
function* foo(){
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for(let v of foo()){
console.log(v)
}
// 1 2 3 4 5
有一点与next
相同,一旦返回的对象的done
属性为true
,for...of
循环就会终止,且不包含该对象,所以上面的return
语句不包括在for...of
循环之中。
Generator.prototype.throw()
Generator函数返回的遍历器对象,都有个throw
方法,可以在函数体外抛出错误,然后在Generator函数体内捕获。如下:
let g = function* (){
try {
yield;
} catch (e) {
console.log('内部捕获', e)
}
}
let i = g();
i.next();
try {
i.throw('a');
i.throw('b')
} catch (e){
console.log('外部捕获', e)
}
Generator.prototype.return()
最大的作用是:终结遍历Generator函数。如下:
function* gen(){
yield 1;
yield 2;
yield 3;
}
let g = gen()
g.next() // {value: 1, done: false}
g.return("foo") // {value: "foo", done: true}
g.next() // {value: undefined, done: true}
使用了return
后,即使done
属性就变成true
了,遍历器就终止,即使后面继续next
也无济于事 ,但值得注意的是,如果reutn
里面带参,那么返回的value
属性的值就是return
的参数。如果不带参,那么value
的值为undefined
next()、throw()、return() 共同点
都是可以让Generator函数恢复执行,并使用不同的语句替换yield
表达式。
如下:
const g = function* (x, y){
let result = yield x + y;
return result;
}
const gen = g(1, 2);
gen.next(); //Object {value: 3, done: false}
gen.next(1); //Object {value: 1, done: true}
gen.throw(new Error('出错了'));
gen.return(2);
上面代码中的第9行,next(1)
相当于将yield
表达式替换成一个数字值1
。如果next()
没有参数,就相当于替换成undefined
。
同理,第11行的替换yield
表达式,相当于第2行变成了let result = new Error('出错了')
。当然,第13行会替换第2行,变成let result = return 2
。
yield* 表达式
定义:可以在一个Generator函数里面执行另一个Generator函数。如下:
function foo(){
yield 'a';
yield 'b';
}
function* bar(){
yield 'x';
yield* foo();
yield 'y';
}
// "x"
// "a"
// "b"
// "y"
还有例子:
function* gen(){
yield* ["a", "b", "c"];
}
gen().next() // {value: "a", done: false}
因为原生数组本身就支持遍历器,因此会遍历数组成员,但如果上面的不加*号,就会返回整个数组。
字符串也一样,因为是原生支持遍历器,所以也能使用这个加了*号的表达式:
function* read(){
yield 'hello';
yield* 'hello';
}
read().next() //"hello"
read().next() //"h"
实际上,数据结构只要有Iterator接口,就可以被yield*
遍历。
Generator含义
协程
定义: 协程(coroutine)是一种程序运行方式,可以理解成“协作的线程 / 协作的函数”。协程即可以用单线程实现,也可用多线程实现,前者是一种特殊的子例程,后者是一种特殊的线程。
- 子例程:
传统的子例程采用堆叠式“后进先出”的执行方式,只有当子函数完全执行完毕,才会结束执行父函数。
- 普通的线程:
有自己的上下文,可以在同一个时间内有多个线程处于运行状态。
- 协程与子例程的差异:
多个线程的情况情况下(单线程的情况下,即多个函数)可以并行执行,但只有一个线程(或函数)处于正运行的状态,其他的线程(或函数)都处于暂停状态,而且线程之间可以交换执行权,意思是一个线程可以执行到一半,然后暂停,并把执行权交给另一个线程执行,等稍后执行回收执行权的时候,再恢复执行。这种并行执行、交换执行权的线程,就称为协程。
- 协程与普通线程的差异:
虽然可以在同一个时间有许多线程同时处于运行状态,但是运行的协程只能有一个,其他的协程都处于暂停状态。并且普通的线程都是抢先式的,哪个线程得到资源,必须由运行环境决定。而协程是合作式的,执行权由协程自己分配。
在ES6里,Generator函数就是ES6对协程的实现,不过并不是完全的实现。Generator函数被称为“半协程”,意思是说只有Generator函数的调用者,才可以把执行权还给Generator函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。
堆栈
JS代码运行的时候,会产生一个全局的上下文,也就是堆栈,然后在这个上下里面如果又一个块级代码或执行函数,又创建一个上下文。由此就形成了个“后进先出”的堆栈数据结果。但Generator函数不是这样的,虽然执行的时候也会创建上下文,但一旦遇到了yield
命令,就会暂时退出堆栈,里面所有变量和对象会冻结当前状态,等调用了next
命令时,这个上下文环境就会重新加入调用栈,恢复执行。
应用操作
因此,Generator可以暂停函数执行,返回任意表达式的值。可应用很多场景。
异步操作
可以把异步操作写在yield
表达式里面,等调用next
方法时再往后执行。如下例子:
function* loadUI(){
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
let loader = loadUI();
loader.next() //加载UI
loader.next() //卸载UI
上面的代码是所有Loading
界面的逻辑,被封装在一个函数,按部就班的执行!