概述
这是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() //加载UIloader.next() //卸载UI
上面的代码是所有Loading界面的逻辑,被封装在一个函数,按部就班的执行!
