一文了解你不知道的JavaScript生成器篇
霍格沃茨魔法师 人气:0前言
在没有JavaScript的生成器概念之前,我们几乎普遍依赖一个假定:一个函数一旦开始执行,就会运行到结束,期间不会有其他代码能够打断它并插入其间。如下代码所示:
var x = 1; function foo(){ x++; bar(); console.log("x",x); } function bar(){ x++; } foo(); //x:3
不过直到ES6引入了一个新的函数类型,发现它并不符合这种运行到结束的特性。这类新的函数被称为生成器。生成器的出现是我们知道原来有时代码并不会顺利的运行,可以通过暂停的方式进行异步回调,让我们摒弃了此前的认知。
了解生成器
下面来看一段合作式并发的ES6代码:
var x = 1; function *foo(){ x++; yield;//暂停 console.log("x",x) } function bar(){ x++; }
可以看到使用了*foo的形式生成这个函数,代表生成器而非常规函数。
现在,我们要如何运行前面的代码片段,使得bar()在*foo()內部的yield处执行呢?
步骤如下:
(1) 首先var it = foo() 构造一个迭代器it来控制这个生成器,这个迭代器会控制它的执行。
(2) 使用it.next() 启动生成器*foo(),并运行了*foo()第一行的x++。
(3) *foo() 在yield语句处暂停,在这一点上使得第一个it.next()调用结束。此时*foo()仍在运行并且是活跃的,但处于暂停状态。
(4) 此刻我们查看x的值,此时为2
(5) 然后我们调用bar(),它通过x++再次递增x。
(6) 此刻我们再次查看x的值,此时为3。
(7) 最后再次调用it.next()调用从暂停处恢复了生成器*foo()的执行,并运行console.log(..)语句,这条语句使用当前的值为3.
显然,foo()启动了,但是并没有完整运行,它在yield处暂停了。后面恢复了foo()并让它运行到结束,但这不是必须的。
因此,生成器就是一类特殊的函数,可以一次或多次启动和停止,并不一定非得要完成。尽管现在还不是特别清楚它的强大之处,但往后我们会看到它将成为构件以生成器作为异步流程控制的代码模式的基础构建之一。
对于生成器函数是一个特殊的函数这个概念,看两个例子来更深入的理解一下:
代码1:
function *foo(x,y){ return x*y; } var it = foo(6,7); var res = it.next(); res.value; //42
代码2:
function *foo(x){ var y = x *(yield); return y; } var it = foo(6); //启动foo() it.next(); var res = it.next(7); res.value //输出什么?
通过对比两个代码其实可以发现它的相似之处。我们主要分析第二个代码。首先,传入6作为参数x。然后调用it.next(),这会启动foo().在foo()內部,开始执行语句var y = x...,但随后就遇到了yield表达式。它很神奇的就会在这一点上暂停*foo(),并在本质上要求调用代码为yield表达式提供一个结果值。接下来,调用it.next(7),这一句把值传回作为被暂停的yield表达式的结果。所以,此时的赋值语句为var y = 6 * 7,现在return这个42作为it.next(7)的结果。
实际上我们考虑的重点是这段代码中的这两行:
var y = x * (yield); return y;
这段代码,在第一个yield这里应该插入什么值呢?由于第一个next()运行,使得生成器启动并运行到此处,所以显然他无法回答这个问题,那么第二次next()调用回答第一个yield提出的这个问题,传入了7。
注意,是第二个next回答第一个yield;
再把代码稍微改动一下:
function *foo(x){ var y = x *(yield “hello”); return y; } var it = foo(6); //启动foo() var res = it.next(); res.value //输出什么? res = it.next(7); res.value //输出什么?
在第一次调用next之后,没有传入任何东西,res.value的值是hello,第二次向上一步暂停的yield处传入7,于是开始了6*7的计算,res.value的值变为42.
这里的每一个next都得到了回应。
小记:在第一次next()调用时没有传入任何值,此时的value就是yield后的数据,第二次向next()传入参数之后把这个参数代入yield处。其实呢,yield和next()这一对组合起来,在生成器的执行过程中构建了一个双向消息传递系统。我们并没有向第一个next()调用发送值,这是有意为之,只有暂停的yield才能接收这样一个通过next()传递的值,而在生成器的起始处我们调用第一个next()时,还没有暂停的yield来接收这样的一个值,所以不要在第一个next()上传递参数。
for...of
就像ES6新增的for...of循环一样,这意味着可以通过原生循环语法自动迭代标准迭代器:
var a = [1,3,5,7,9] for(var v of a){ console.log(v); //1 3 5 7 9 }
for...of循环在每次迭代中自动调用next(),他不会向next()传入任何值,并且会在接收到done:true之后手动停止,这对于在一组数据上循环很方便。循环向a请求它的迭代器,并自动使用这个迭代器迭代遍历a的值。
iterable(可迭代)
从ES6开始,从一个iterable中提取迭代器的方法是:iterable必须支持一个函数,其名称是专门的ES6符号值Symbol.iterator。调用这个函数时,它会返回一个迭代器,通常每次调用会返回一个全新的迭代器,虽然这一点并不是必须的。就像前面使用for...of直接迭代的一样,我们使用迭代器重写:
var a = [1,3,5,7,9] var it = a[Symbol.iterator]() it.next().value;//1 it.next().value;//3 it.next().value;//5
生成器+promise
ES6中最完美的世界就是生成器和promise的结合。但如何实现呢?
让我们来试一下,把支持promise的foo()和生成器*main()放在一起:
function foo(x,y){ return request( "http:url/?x+y" ) } function *main(){ try{ var text = yield foo(1,2) console.log(text) } catch(err){ console.error(err) } }
现在如何运行*main()呢?还有一些实现细节需要补充,来实现接收和连接yield出来的promise,使它能够在决议之后恢复生成器,先从手工开始实现:
var it = main() var p = it.next().value //此时p为foo(1,2) p.then( //等待promise的p决定成功/拒绝 function(){ it.next(text) }, function(err){ it.throw(err) } )
这个模式下生成器yield出promise,然后其控制生成器的迭代器来执行它,直到结束,是非常强大有用的一种方法。对于ES7中,在这一方面增加语法支持的提案已经有了一些很强势的支持。
async与await
function foo(){ return request( "http:url/?x+y" ) } async function main(){ try{ var text = await foo(1,2) console.log(text) } catch(err){ console.log(err); } } main();
可以看到main不再被声明为*main生成器函数,它现在是一类新的函数:async函数,并且我们也不用yield暂停点来暂停等待了,而是使用await等待并决议。我们await了一个promise,async函数就会自动获知要做什么,它会暂停这个函数(就像yield),直到promise生成成功/拒绝的结果。
小结
生成器是ES6的一个新的函数类型,它并不像普通函数那样总是从运行开始到运行结束。取而代之的是,生成器yield可以在运行当中暂停,并且等到将来再次next()时再从暂停的地方恢复运行。
这种交替的暂停和恢复是合作式的双向消息传递,这意味着生成器具有独一无二的能力来暂停自身,这是通过关键字yield实现的。不过,只有控制生成器的迭代器具有恢复生成器的功能(比如next())
yield和next()这一对不只是一种控制机制,实际上也是一种双向消息传递机制。yield..表达式本质是暂停下来等待某个值,接下来的next()调用会向被暂停的yield表达式传回一个值(或者是隐式的undefined)
有时,我们还会把可能的异步藏在yield后面,把异步移动到控制生成器的迭代器的代码部分,如yield foo(1,2)。换句话说,生成器为异步代码保持了顺序、同步、阻塞的代码模式,这使得大脑可以更自然地追踪代码,解决了基于回调的异步的缺陷。
加载全部内容