你不知道的JavaScript (中卷)2
#
异步和性能如何表达和控制持续一段时间的程序行为。是指程序的一部分现在运行,而另一部分在将来运行——现在和将来之间有间隙,在这段间隙中,程序没有活跃。 所有重要的程序(特别是 JavaScript 程序)都需要通过这样或那样的方法来管理这段时间间隙,这时可能是在等待用户输入、从数据库或文件系统中请求数据、通过网络发送数据并等待响应。在诸如此类的场景中,程序都需要管理这段时间间隙的状态。 程序中现在运行的部分和将来运行的部分之间的关系,就是异步编程的核心
#
事件循环JavaScript 引擎并不是独立运行的,它运行在宿主环境中,对多数开发者来说通常就是 Web 浏览器。最近几年 JavaScript 已经超出了浏览器的范围,进入了其他环境。比如通过像 node.js 这样的工具进入服务器领域。但是这些所有环境都有一个共同的“点”,即他们都提供了一种机制来处理程序中多个块的执行,且执行每块时调用 JavaScript 引擎。这种机制被称为事件循环。 换句话说,JavaScript 引擎本身并没有时间的概念,只是一个按需执行 JavaScript 任意代码片段的环境。“事件”(JavaScript 代码执行)调度总是由包含它的环境进行。
setTimeOut(..)
并没有把你的回调函数挂在事件循环队列中,他所做的是设定一个定时器。当定时器到时,环境会把你的回调函数放在事件循环中,这样在未来某个时刻的 tick 会摘下并执行这个回调。假如此时事件循环中已经有 100 个项目了怎么办?它就得排在其他项目之后。这也解释了为什么setTimeOut(..)
定时器的精度可能不高。只能保证你的回调函数不会在指定的时间间隔之前运行,可能在那个时刻,也可能在那个时刻之后运行。
#
并行线程术语“异步”和“并行”常常被混为一谈,但实际上它们的意义完全不同。异步是现在和将来的时间间隙。而并行是关于能够同时发生的事情。
并行计算最常见的工具就是进程和线程。进程和线程独立运行,并可能同时运行:在不同的处理器,甚至不同的计算机上,但多个线程能够共享单个进程的内存。与之相对的是,事件循环把自身的工作分成一个个任务并顺序执行,不允许对共享内存并行访问和修改。通过分立线程中彼此合作的事件循环,并行和顺序执行可以共存。
并行线程的交替执行和异步事件的交替调度,其粒度是完全不同的。
var a = 20;function foo() { a = a + 1;}function bar() { a = a * 2;}
ajax("http://some.url.1", foo);ajax("http://some.ur2.1", bar);
完整运行 JavaScript 单线程运行特性,所以 foo()和 bar()中的代码具有原子性。也就是说,一旦 foo()开始运行,它的所有代码都会在 bar()中的任意代码运行之前完成。或者 bar()比 foo()运行的完整。这称为:完整运行 特性。
但是,同一段代码有两个可能输出意味着还是存在不确定性!!~但是,这种不确定性是在函数(事件)顺序级别上,而不是多线程情况下的语句顺序级别(或者说,表达式运算顺序级别)。换句话说,这一确定性要高于多线程情况。 在 JavaScript 特性中,这种函数顺序的不确定性,就是通常所说的竞态条件,foo()和 bar()相互竞争,看谁先运行。具体的说,因为无法可靠预测 a 和 b 的最终结果,所以才是竞态条件。
单线程事件循环是==并发==的一种形式。
不交互 两个或多个“进程”在同一个程序内并发地交替运行它们的步骤/事件时,如果这些任务彼此不相关,就不一定需要交互。如果进程间没有相互影响的话,不确定性是完全可以接受的。
交互 并发的“进程”需要相互交流,通过作用域或 DOM 间接交互。如果出现这样的交互,就需要对它们的交互进行协调以避免竞态的出现。
协作 一种并发合作方式,称为“并发协作”。 如下情况:
var res = [];
function response(data) { res = res.concat( //创建一个新的变换数组并把所有data值加倍 data.map(function(val) { return val * 2; }) );}
ajax("http://some.url.1", response); //异步的ajax请求
当 ajax 请求返回结果之后,整个列表会映射到 res 中,如果记录只有几千条,就没什么关系,如果有 1000w 条记录的话,就可能需要运行很长时间了。这样的“进程”运行时,页面上的其他代码都不能运行,包括不能有其他的 response 调用或 UI 刷新,甚至像滚动、输入、按钮点击这样的用户事件。
所以要创建一个协作性更强更友好且不会霸占事件循环队列的并发系统,可以异步地批处理这些结果。每次处理之后返回事件循环,让其他等待事件有机会运行。如下:
var res = [];
function response(data) { var chunk = data.splice( 0, 1000 ); //分块,将数据一次运算1000个
res = res.concat( chunk.map( function(val) { return val * 2; }); );
if(data.length > 0) { //如果还有数据,就接着调用 setTimeout( function() { response( data ); }, 0); }}ajax("http://some.url.1", response); //异步的ajax请求
把数据集合放在最多包含 1000 条项目的块中。这样,就确保了“进程”运行时间很短,即使这意味着需要更多后续“进程”,因为事件循环的交替运行会提高站点/APP 的响应(性能)。这里使用 setTimeout(..,0),进行异步调度,意思就是“把这个函数插入到当前事件循环队列的结尾处”。
任务: 对于任务队列最好的理解方式:它是挂在事件循环队列的每个 tick 之后的一个队列。在事件循环的每个 tick 中,可能出现的异步动作不会导致一个完整的新事件添加到事件循环队列中,而会在当前 tick 的任务队列末尾添加一个项目。(一个任务)。提一下,promise 的异步特性是基于任务的。
小结 实际上,JavaScript 程序总是至少分为两块:第一块现在执行,下一块将来运行,以响应某个事件。尽管程序是一块一块执行的,但是所有这些块共享对程序作用域和状态的访问,所以对状态的修改都是在之前累积的修改之上进行的。 一旦有事件需要运行,事件循环就会运行,直到队列清空。事件循环的每一轮称为一个 tick。用户交互、IO 和定时器会向事件队列中加入事件。 任意时刻,一次只能从队列中处理一个事件。执行事件的时候,可能直接或间接地引发一个或多个后续事件。 并发是指两个或多个事件链随事件发展交替执行,以至于从更高的层次来看,就像是同时在运行(尽管在任意时刻只处理一个事件)。 通常需要对这些并发执行的“进程”(和操作系统中的进程不同)进行某种形式的交互协调,比如需要确保执行顺序或者需要防止竞态出现。这些“进程”也可以通过把自身分割为更小块,以便于其他“进程”插入进来。
回调地狱: 我们的顺序阻塞式的大脑计划行为无法很好地映射到面向回调的异步代码。这就是回调方式最主要的缺陷:对于它们在代码中表达异步的方式,我们的大脑需要努力才能同步得上。
Node 风格: 有一种常见的回调模式叫做“error-first 风格”,有时也叫 Node 风格,因为几乎所有的 Node.js API 都会采用这种风格。其中回调的第一个参数保留用作错误对象(如果有的话),如果成功的话,这个参数就会被清空/置假(后续的参数就是成功数据)。不过,如果产生了错误结果,那么第一个参数就会被置起/置真(通常就不会再传递其他结果):
function response(err, data) { if (err) { //出错? console.error(err); } else { //否则认为成功 console.log(data); }}ajax("http://some.url.1", response);
下面展示一个工具:验证概念版本
function timeoutify(fn, delay) { //这个setTimeout会隔一段delay的时间 //再把这个function放到任务队列里面 //到了时间,就抛出timeout错误 var intv = setTimeout(function() { intv = null; fn(new Error("Timeout!")); }, delay);
//return的这个函数算是闭包了 //里面有intv这个东西,会提升到全局变量吧 return function() { //返回的函数判断一下intv是否存在 //如果超时了delay就会置为null //没超时的话就把定时器清除掉 if (intv) { clearTimeout(intv); fn.apply(this, arguments); } };}
//使用方式function foo(err, data) { if (err) { console.error(err); } else { console.log(data); }}
ajax("http://someurl.1", timeoutify(foo, 500));
ajax 异步调用请求需要耗费时间,如果响应很快,500ms 以内就能响应,那就调用了timeoutify
返回的那个函数,在函数里面执行相应的逻辑:500ms 还没到, 定时器的任务还没执行,intv 还是真值,判断后清除这个定时器,然后让 foo 函数在 ajax 回调的环境中执行。猜测,ajax 必定至少返回两个参数,一个是 err 的位置,返回的必然是 null,另一个是 data 的位置,应该返回相应的请求数据。
小结 回调函数是 JavaScript 异步的基本单元。但是随着 JavaScript 越来越熟,对于异步编程领域的发展,回调已经不够用了。
- 大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流程的方式是非线性的,非顺序的,这使得正确推导这样的代码难度很大。难于理解的代码是坏代码,会导致 bug。
- 更重要的一点是,回调会受到控制翻转的影响!因为回调会把控制权交给第三方(通常是不受控制的第三方工具!)来调用代码中的
continuation
。这种控制转移导致一系列麻烦的信任问题,比如回调的次数是否会超出预期。
可以发明一些特定的逻辑来解决这些信任问题,但是其难度高于应有的水平,可能会产生更笨重、更难维护的代码。并且缺少足够的保护,其中的损害要直到受到 bug 的影响才会被发现。
#
promise一但我需要的值准备好了,我就用我的承诺值换取这个值本身。
从外部看,由于 Promise 封装了依赖于时间的状态——等待底层值的完成或拒接,所以 Promise 本身是与时间无关的。因此,Promise 可以按照可预测的方式组成(组合),而不用关心时序或底层的结果。
一旦 Promise 决议,它就永远保持在这个状态。此时它就成为了不变值,可以根据需求多次查看。Promise 决议后就是外部不可变的值,我们可以安全地把这个值传递给第三方,并确信它不会被有意无意地修改。特别是对于多方查看同一个 Promise 决议的情况,尤其如此。 Promise 是一种封装和组合未来值的易于复用的机制。
#
具有 then 方法的鸭子类型如何判断某个值是不是 promise?既然 promise 是通过 new promise(..)语法创建的你可能认为使用 p instanceof Promise
来判断,但是这种方式不足以作为检查方法。最重要的原因是,Promise 值可能是从其他浏览器窗口(iframe 等)接收到的,这个浏览器窗口自己的 Promise 可能和当前窗口/frame 的不同,因此这样的检查无法识别 Promise 实例。此外,库和框架可能会选择实现自己的 Promise,而不是使用原生 ES6 Promise 实现。实际上,很可能使用的是早期根本没有 Promise 实现的浏览器中使用由库提供的 Promise。
根据一个值的形态(具有哪些属性)对这个值的类型作出一些假定。这种类型检查一般用术语鸭子类型来表示——(如果它看起来像只鸭子,叫起来像只鸭子,那它一定就是鸭子),于是检测大概就是这样:
if( p !== null && ( typeof p === "object" || typeof p == "function" ) && typeof p.then === "function") { //假定这就是一个thenable}else { //不是一个thenable}
假如在原型中加入了 then 函数,那么生成的对象就会被认定为 thenable 的。
Object.prototype.then = function() {};Array.prototype.then = function() {};var v1 = { hello: "world" };var v2 = [ "hello", "world"];
v1 和 v2 都会被认作是 thenable。如果有任何代码无意或恶意地给 Object.prototype、Array.prototype 或任何原生原型添加 then(..),如果有 Promise 决议到这样的值,就会永远挂住。
#
Promise 的信任问题把一个回调传入工具 foo(..)时,可能会出现如下问题:
- 调用回调过早
- 调用回调过晚(或不被调用)
- 调用回调次数过少或过多
- 未能传递所需的环境和参数
- 吞掉可能出现的错误和异常 Promise 的特性就是专门用来为这些问题提供一个有效的可复用的答案。
调用过早 根据定义,promise 就不必担心这种问题,因为即使是立即完成的 Promise,如下:
new Promise( function(resolve) { resolve(32);})
也无法被同步观察到。也就是说,对一个 Promise 调用 then(...)的时候,即使这个 Promise 已经决议,提供给 then(..)的回调的总会被异步调用。
调用过晚 Promise 创建对象调用 resolve(..)或 reject(..)时,这个 promise 的 then(..)注册的观察回调就会被自动调度。一个 promise 决议后,这个 promise 上所有的通过 then(..)注册的回调都会在下一个异步时机点上依次被立即调用。这些回调中的任意一个都无法影响或延误对其他回调的调用。
回调未调用 没有任何东西(甚至是 JavaScript 错误)能阻止 Promise 向你通知它的决议(如果它决议了的话)。如果对一个 Promise 注册了一个完成回调和一个拒绝回调,那么 Promise 在决议时总是会调用其中的一个。
调用次数过多或过少 根据定义,回调被调用的正确次数应该是 1.“过少”的调用次数就是 0 次,和前面解释的唯美调用是一个道理。“过多”的情况很容易解释。Promise 的定义方式使得它只能被决议一次,如果出于某种原因,Promise 创建的代码试图调用 resolve(..)或 reject(..)多次,或者试图两者都调用,那么这个 Promise 将只会接受第一次决议,并默默地忽略任何后续调用。由于 Promise 只能被决议一次,所以任何通过 then(..)注册的(每个)回调就只会被调用一次。
未能传递参数/环境值
Promise 最多只能有一个决议值(完成或拒绝)。
如果没有用任何值显式决议,那么这个值就是undefined
,这是 JavaScript 常见的处理方式。但不管这个值是什么,无论当前或未来,他都会被传给所有注册的(且适当的完成或拒绝)回调。
此外,如果使用多个参数调用 resolve(..)或者 reject(..),第一个参数之后的所有参数都会被默默忽略。如果要传递多个值,就必须要把它们封装在单个值中传递,比如通过一个数组或对象。
吞掉错误或异常 如果拒绝一个 promise 并给出一个理由(也就是一个出错信息),这个值就会被传给拒绝回调。如果在 promise 的创建过程中或在查看决议过程中的任意时间点,出现了一个 JavaScript 异常错误,比如一个 TypeError 或 ReferenceError,那这个异常就会被捕捉,并且会使这个 promise 被拒绝。
链式流
Promise并不只是一个单步执行this-then-that操作的机制,我们可以把多个Promise连接到一起以表示一系列异步步骤。
这种方式可以实现的关键在于以下两个Promise固有行为特性:
- 每次对Promise调用then(..),他都会创建并返回一个新的promise,我们可以将其链接起来。
- 不管从then(..)调用的完成回调(第一个参数)返回的值是什么,它都会被自动设置为被链接Promise(第一点中的)的完成。
一个链式的demo:
var p = Promise.resolve( 21 );
p.then( function(v) { console.log( v ); //创建一个promise并返回 return new Promise( function( resolve, reject ) { //这里是一些异步操作 setTimeout( function() { resolve( v * 2); }, 1000); });}).then( function(v) { //上一个promise1000ms后执行这个 console.log( v ); //42})
将这种链式的异步链接封装为一个工具,以便多个步骤调用,如下:
function delay(time) { return new Promise( function(resolve, reject) { setTimeout(resolve, time ); });}
delay( 100 ) //步骤1.then( function STEP2(){ console.log("在第一步过后的100ms,进行第二步"); return delay( 200 );}).then( function STEP3(){ console.log("第二步过后的200ms,进行第三步"); return delay( 300 );}).then( function STEP4() { console.log( "第三步过后的300ms,进行第四步");})
假如promise链中的某个步骤出错了会怎么样?错误和异常是基于每个promise的,这意味着可能在链的任意位置捕捉到这样的错误,而这个捕捉动作在某种程度上就相当于将整条链“重置”到了正常运作,如下:
//前置操作
function request(url) { return new Promise( function(resolve, reject) { //ajax()回调应该是我们这个promise的resolve函数 ajax( url, resolve); });}
//步骤1request( "http://some.url.1/" );
//步骤2.then( function(response1){ foo.bar(); //undefined出错 //永远走不到这里 return request( "http://some.url.2/?v=" + response1 );})
//步骤3.then( function fulfilled(response2) { //永远不会到这里 }, function rejected(err) { console.log( err ); //来自foo.bar()的错误TypeError return 42; })
//步骤4.then( function(msg) { console.log( msg ); //42 上一个步骤的错误返回值});
第2步出错后,第3步的拒绝函数会捕捉到这个错误。如果有拒绝处理函数的返回值的话,会用来完成交给下一个步骤的promise。这样,这个链现在就回到了完成状态。
如果没有给then(..)传递一个适当有效的函数作为完成处理函数参数,还是会有作为替代的一个默认处理函数:
var p = Promise.resolve( 42 );
p.then( //假定的完成处理函数,如果省略或者传入任何非函数值 //function(v) { // return v; //} null, function reject( err ) { //到不了这里,因为promise直接被resolve了 });
默认的完成处理函数,会把接收到的任何传入值传递给下一个步骤(promise).
then(null, function(err){..})
这个模式——只处理拒绝,但又把完成值传递下去,它有一个缩写形式的API:catch(function(err) {..})
总结一下使链式流程控制可行的Promise固有特性:
- 调用Promise的then(..)会自动创建一个新的Promise从调用返回
- 在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的)Promise就响应地决议。
- 如果完成或拒绝处理函数返回一个Promise,他会将被展开,这样一来,无论它的决议值是什么,都会成为当前then(..)返回的链接promise的决议值。
Promise.resolve()
会将传入的真正Promise直接返回,对传入的thenable
则会展开。
术语:决议、完成以及拒绝
看一下Promise的构造器:
var p = new Promise( function(X,Y){ // X(); //用于完成 // Y(); //用于拒绝} );
第一个函数标识Promise已经完成,第二个标识Promise被拒绝。代码的名称只是一个标识符,对引擎而言没有任何意义。
错误处理
同步处理错误,使用try ..catch
;异步处理错误,有一种error-first
回调风格:
function foo(cb) { setTimeout( function() { try { var baz.bar(); cb( null, x); //成功 } catch (err) { cb(err) } }, 100);}
foo( function(err, val) { if(err) { console.log( err ); } else { console.log( val ); }})
写一个函数,传入的参数是一个回调函数,业务逻辑都在函数内部,只有按照逻辑完成或者出错后,给回调函数传递相应的参数。上面的例子,只有在baz.bar()调用会同步地立即成功或失败的情况下,try..catch
才能工作,如果baz.bar()本身有自己的异步完成函数,其中的任何异步错误都将无法捕捉。
门机制是要等待两个或更多并行/并发的任务都完成才能继续。完成的顺序并不重要,但是必须都要完成。Promise.all([..])需要一个参数,是一个数组,通常由promise实例组成。从Promise.all([..])调用返回的promise会收到一个完成消息。这是一个由所有传入promise的完成消息组成的数组,与指定顺序一致(与完成顺序无关)。传给Promise.all([..])的数组中的值可以是Promise、thenable,甚至是立即值。列表中的每个值都会通过Promise.resolve(..)过滤,以确保等待的是一个真正的Promise。
有时有这种需求,只响应第一个跨过终点线的Promise,而抛弃其他的Promise,这就需要用到——Promise.race([..])
,这中模式传统上称为门闩,但在Promise中称为竞态。
一旦有任何一个Promise决议为完成,Promise.race([..])就会完成;一旦有任何一个Promise决议为拒绝,他就会拒绝。
var p1 = new Promise( function(resolve, reject) { setTimeout(()=>{ reject("promise1")}, 500);})
var p2 = new Promise( function(resolve, reject) { setTimeout( ()=> { resolve("promise2")}, 1000);})
Promise.race([p1,p2]).then( (msg)=>{console.log(msg)} )
这里写一个demo试一下,确实是第一个决议后的promise,无论是完成还是拒绝,都会返回第一个完成的结果。
all([..])和race([..])的变体
- none([..]),这个模式类似于all([..]),不过完成和拒绝的情况互换了,所有的Promise都要被拒绝,即拒绝转化为完成值。
- any([..]),这个模式和all([..])类似,但是会忽略拒绝,所以只需要完成一个而不是全部。
- first([..]) 这个模式类似于与any([..])的竞争。即只要第一个Promise完成,它就会忽略后续的任何拒绝和完成。
- last([..])这个模式类似于first([..]),但却是最后一个完成胜出。
#
Promise API概述new Promise(..)构造器
var p = new Promise( function(resovle, reject) { //resolve(..) 用于决议/完成这个promise //reject(..) 用于拒绝这个promise})
reject(..)就是拒绝这个promise,但是resolve(..)既可能完成promise也可能拒绝。要根据传入的参数而定。如果传给resolve(..)的是一个非Promise、非thenable的立即值,这个promise就会用这个值完成。但是如果传给resolve(..)的是一个真正的Promise或thenable值,这个值就会被递归展开,并且(要构造的)promise将取用其最终决议值或状态。
then(..)和catch(..)
每个promise实例(不是promise API命名空间)都有then(..)和catch(..)方法,通过这两个方法可以为这个Promise注册完成和拒绝处理函数。Promise决议之后,立即回调用这两个处理函数之一,但不会两个都调用,而且总是异步调用。
then(..)接受一个或两个参数:第一个用于完成回调,第二个用于拒绝回调。如果两者中的任何一个被省略或者作为非函数值传入的话,就会替换为相应的默认回调。默认完成回调只是把消息传递下去;默认拒绝回调则只是重新抛出(传播)其接收到的出错原因。
如果向Promise.all([..])传入空数组,它会立即完成, 但Promise.race([..])会挂住,且永远不会决议。
#
Promise的局限性- 顺序错误处理
在promise链中,假如中间的某个promise出了错误,链中的任何地方的任何错误都会在链中一直传播下去,直到被查看。
var p = foo(42);.then( STEP2).then( STEP3);
这里的p并不指向链中的第一个promise,而是指向最后一个promise,即来自调用then(STEP3)的那一个。很多时候并没有为Promise链序列的中间步骤保留的引用,没有这样的引用就无法关联错误处理函数来可靠地检查错误。
- 无法取消的Promise
一旦创建了一个Promise并为其注册了完成或拒绝函数,如果出现某种情况使得这个任务悬而未决的话,开发者也没有办法从外部停止它的进程。
如果说Promise确实有一个真正的性能局限的话,那就是它们没有真正提供可信任性保护支持的列表以供选择(你总是得到全部)。
- 小结
promise解决了我们因只用回调的代码而备受困扰的控制反转问题。
是它们并没有摒弃回调,只是把回调的安排转交给了一个位于我们和其他工具之间的可信任的中介机制。
#
生成器先看一段合作式并发的ES6代码:
var x = 1;function *foo() { x++; yield; //暂停 console.log("x:" , x);}function bar() { x++;}
//下面构造一个迭代器it来控制这个生成器var it = foo();
it.next(); //启动foo()x; //2bar(); x; //3it.next(); //x: 3
运行流程:
- it = foo()运算并没有执行生成器 *foo(),而只是构造了一个迭代器,这个迭代器会控制它的执行。
- 第一个it.next()启动了生成器
*foo()
,并运行了*foo()
的第一行的x++。 *foo()
在yield语句处暂停,在这里第一个it.next()的调用结束。此时*foo()
仍然是活跃的,但处于暂停状态。- 查看x的值,此时为2.
- 调用bar(),它通过x++再次递增x。
- 再次查看x的值,此时为3.
- 最后的it.next()调用从暂停处恢复了生成器
*foo()
的执行,并运行console.log(..),此时当前x的值是3.
显然foo()启动了,但是没有完整运行。它在yield处暂停了。后面恢复了foo()并让它运行到结束。但这不是必须的。生成器就是一类特殊的函数,可以一次或多次启动和停止,并不一定非得要完成。
生成器的迭代消息传递
除了能够接受参数并提供返回值之外,生成器甚至提供了更强大的内建消息输入输出能力,通过yield和next(..)实现。
function *foo(x) { var y = x * (yield); return y;}var it = foo(6);
it.next(); //启动foo(..)var res = it.next(7); //传参
res.value; //42
首先传入6作为参数x。然后调用it.next(),这会启动*foo(..)
,在*foo(..)
内部,开始执行赋值语句,但随后就遇到了一个yield表达式,它就会在这一点上暂停*foo(..)
,并在本质上要求调用代码为yield表达式提供一个结果值。接下来,调用it.next(7),这一句把值7传回作为被暂停的yield的表达式。
yield和next(..)调用有一个不匹配,一般来说,next()要比yield语句多一个。为什么呢?因为第一个next(..)总是启动一个生成器,并运行到第一个yield处。不过,是第二个next(..)调用完成第一个被暂停的yield表达式,第三个next(..)调用完成第二个yield,以此类推。
消息是双向传递的——yield..作为一个表达式可以发出消息响应next(..)调用,next(..)也可以向暂停的yield表达式发送值。
function *foo(x) { var y = x * (yield "Hello"); return y;}
var it = foo(6);var res = it.next(); //第一个next,开启生成器res.value // "Hello"
res = it.next(7); //向等待的yield传入7res.value; //42
yield..和next(..)这一对组合起来,在生成器的执行过程中构成了一个双向信息传递系统。
我们并没有向第一个next()调用发送值,只有暂停的yield才能接受这样一个通过next(..)传递的值,而在生成器的起始处我们调用第一个next()时,还没有暂停的yield来接受这样一个值。规范和所有兼容浏览器都会默默丢弃传递给第一个next()的任何东西。
next()提出问题,yield回答问题。但是next()比yield要多一个,最后就由return来回答最后一个问题!