本文最后更新于:8 个月前
Generator 函数是如何实现异步编程的?Generator除了作为异步编程的实现方案之外,还有哪些使用场景?Generator与Iterator之间存在着什么关联?Generator的关键字和方法有哪些?
ES6(六)Generator函数 一、简介 Generator 函数是 ES6 提供的一种异步编程解决方案,Generator函数在形式上有两个特征:
function
关键字与函数名之间有一个星号
函数体内部使用yield
表达式,定义不同的内部状态
执行Generator函数返回的是一个遍历器对象。该对象具有两个属性:value
和done
,前者表示yeild
表达式返回的值,后者表示遍历是否结束。该对象有一个next()
方法,必须调用一次它,函数才开始执行,然后执行到遇到yield
关键字时停止,但是会将yield
关键字右边的表达式执行结果返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function * helloWorldGenerator ( ) { console .log ('process 1' ) yield 'hello' ; console .log ('process 2' ) yield 'world' ; console .log ('process 3' ) return 'ending' ; console .log ('process 4' ) }var hw = helloWorldGenerator (); hw.next () hw.next () hw.next () hw.next ()
二、yield 表达式 yield 表达式是提供给Genrator函数,用于暂停遍历执行的函数。遍历器对象的next
方法的运行逻辑如下:
遇到yield
表达式,暂停向下执行,并把yield
右边的表达式的值返回返回对象的value
的值
下一次调用next()
,继续向下执行,直到遇到yield
如果没有遇到新的yield
,就一直执行到结束,或者到return
为止,将return
后面表达式的值作为value
返回;如果没有return
语句,那value
的值就是undefined
注意:yield只能在Generator函数中使用!
另外,yield
表达式如果用在另一个表达式之中,必须放在圆括号里面。
1 2 3 4 5 6 7 function * demo ( ) { console .log ('Hello' + yield ); console .log ('Hello' + yield 123 ); console .log ('Hello' + (yield )); console .log ('Hello' + (yield 123 )); }
yield
表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
1 2 3 4 function * demo ( ) { foo (yield 'a' , yield 'b' ); let input = yield ; }
三、与Iterator的关系 任意一个对象的Symbol.iterator
方法,等于该对象的遍历器生成函数。
而 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator
属性,从而使得该对象具有 Iterator 接口。然后就可以实现一些 Iterable 可遍历对象 才能执行的操作,如:使用扩展操作符、for…of循环遍历
1 2 3 4 5 6 7 8 9 10 11 12 var myIterable = {}; myIterable[Symbol .iterator ] = function * () { yield 1 ; yield 2 ; yield 3 ; }; [...myIterable] {...myIterable} for (var x of myIterable) { console .log (x) }
四、方法 1. next方法 yield
和next()
两者结合,可以实现 Generator 函数的双向通信。
yield
可以将它右边表达式的值作为返回对象的value
属性传递出来,但是yield
表达式本身没有返回值。
1 2 3 4 5 6 7 8 function * f ( ){ let x = yield 1 console .log (x) }var g1 = f () g1.next () g1.next ()
但是,next()
方法可以带一个参数,该参数会被作为上一个yield
表达式的返回值
1 2 3 4 5 6 7 function * f ( ){ let x = yield 1 console .log (x) }var g2 = f () g2.next () g2.next (2 )
2. throw方法 Generator 函数返回的遍历器对象,都有一个throw
方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获,只能捕获一次,捕获错误后立即结束遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var g = function * () { try { yield 1 yield 2 } catch (e) { console .log ('内部捕获' , e); } };var i = g (); i.next (); try { i.throw ('a' ); i.next () i.throw ('b' ); } catch (e) { console .log ('外部捕获' , e); }
3. return方法 返回给定的值,并且终结遍历 Generator 函数。
五、yield* 表达式 ES6 提供了yield*
表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function * foo ( ) { yield 'a' ; yield 'b' ; }function * bar ( ) { yield 'x' ; yield * foo (); yield 'y' ; }for (let v of bar ()){ console .log (v); }
六、Generator函数的应用场景
作为异步编程的解决方案,这个下面将详进一步介绍。
用来部署Iterator接口,赋值给指定对象的Symbol.iterator
属性。任何一个对象的Symbol.iterator
属性都是一个遍历器生成器,而Generator函数就是遍历器生成函数。
作为一种数据结构。更确切地说,可以看作是一个数组结构,因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口。
七、Generator的异步应用
Generator函数的 yield 可以暂停函数的执行
yield 表达式可以向函数外传递数据,next()方法可以通过参数向函数内传递数据
利用以上这两点,可以将一个异步任务,同步化。下面是一个手动将异步转为同步的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function * gen ( ){ var url = 'https://api.github.com/users/github' ; var result = yield fetch (url); console .log (result); }var g = gen ()var result = g.next () result.value .then (function (data ){ return data.json () }).then (function (data ){ g.next (data) })
分析以上过程,首先是Generator交出执行权给fetch函数,然后fetch函数执行完后,在回调函数中调用next方法,将执行权交还给Generator函数。执行权的交接,就是异步代码转为同步代码的关键步骤。
下面手动模拟一下Generator的执行流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var fs = require ('fs' );var thunkify = require ('thunkify' ); var readFileThunk = thunkify (fs.readFile );var gen = function * (){ var r1 = yield readFileThunk ('/etc/fstab' ); console .log (r1.toString ()); var r2 = yield readFileThunk ('/etc/shells' ); console .log (r2.toString ()); };var g = gen ()var r1 = gen.next () r1.value (function (err,data ){ if (err) throw err var r2 = g.next (data) r2.value (function (err,data ){ if (err) throw err g.next (data) }) })
但是上面是手动执行的,能不能自动实现执行权的交换(流程管理)呢?当然可以,只需要将回调函数不断传入next方法的value属性就行了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function run (fn ){ var gen = fn () function next (err,data ){ var result = gen.next (data) if (result.done ) return result.value (next) } next () }function * g ( ){}run (g)