1 异步:现在与将来
-
1.1 分块的程序
任何时候,只要把一段代码包装成一个函数,并指定其在响应某个时间时执行,就是在代码中创建了一个将来执行的块,也由此在这个程序中引入了异步机制。
-
1.2 事件循环
// 事件循环伪代码 // eventLoop是一个用作队列的数组(先进先出) var eventLoop = [], event; // 永远执行 whie(true) { // 一次tick if(eventLoop.length > 0) { event = eventLoop.shift(); // 拿到队列中的下一个事件 // 执行下一个事件 try { event(); } catch(err) { reportError(err); } } }
-
1.3 并行线程
异步是关于现在和将来的时间间隙,而异步是关于能够同时发生的事情。
事件循环把自身的工作分成一个个任务并顺序执行,不允许对共享内存的并行访问和修改。通过分立线程中彼此合作的事件循环,并行和顺序执行都可以共存。由于JavaScript的单线程特性,foo()(以及bar())中的代码具有原子性。即一旦foo()开始运行,它的所有代码都会在bar()中的任意代码运行之前完成,或者相反。这被称为完整运行特性。
var a = 1, b = 2; function foo() { a ++; b = b * a; a = b+ 3; } function bar() { b--; a = 8 + b; b = a * 2; } ajax("http://some.url.1", foo); ajax("http://some.url.2", bar); // 只会出现两个结果 // 但这种函数顺序的不确定性被称为竞态条件,foo()和bar()相互竞争,看谁先运行。
-
1.4 并发
两个或多个“进程”同时执行就出现了并发,不管组成它们的单个运算是否并行执行。可以把并发看做“进程”级的并行,与运算级的并行(不同处理器上的线程)相对。
-
1.4.1 非交互
两个或多个“进程”在同一个程序内并发地交替运行他们的步骤/事件时,如果这些任务彼此不想管,就不一定需要交互。如果进程间没有相互影响的话,不确定性是完全可以接受的。var res = {}; function foo(result) { res.foo = result } function bar(result) { res.bar = result } ajax("http://some.url.1", foo); ajax("http://some.url.2", bar); // 不管按哪种顺序执行,都不会互相影响。
-
1.4.2 交互
并发的“进程”需要互相交流,通过作用域或DOM间接交互,需要对它们的交互进行协调,避免竞态的出现。var res = []; function cb(data) { res.push(data); } ajax("http://some.url.1", cb); ajax("http://some.url.2", cb); // 假定预期希望res[0]存放url.1返回的结果,res[1]存放url.2返回的结果。但当url.2先返回数据时,结果就会与预期相反。这种不确定性可能就是一个竞态条件bug。 // 可以通过判断url,来存放data
-
1.4.3 协作
并发协作的重点,是取到一个长期运行的“进程”,并将其分割成多个步骤或多批任务,使得其它并发“”进程有机会将自己的运算插入到事件循环队列中交替运行。
-
2 回调
-
2.1 continuation(延续)
回调函数包裹/封装了程序的延续(continuation)。
以回调函数的形式引入单个continuation(或者更多),就容许了大脑工作方式(同步/线性)和代码执行方式(异步)的分歧。这就会致使代码变得难以理解、追踪、调试和维护。 -
2.2 顺序的大脑
一心多用,更可能是大脑执行多个任务时,在进行快速的上下文切换,同时处理每个任务的微小片段。以至于对外界来说,就像是在并行地执行所有任务。
同步的大脑计划,能够很好地映射到同步代码语句,但当代码通过回调表达异步时,并不能很好地映射到同步的大脑计划。
-
2.2.2 嵌套回调与链式调用
多层嵌套回调常被称为回调地狱(callback hell)。
而如果把嵌套回调单独写成一个独立函数时,又会造成代码的难以追踪、调试、维护等。
同样,通过手工硬编码(即使包含了硬编码的出错处理),即指定(预先计划)了所有的可能事件和路径,代码会变得非常复杂,以至于无法维护和更新。doA( function(){ doB(); doC( function(){ doD(); } ) doE(); }); doF(); // 当上述操作回调都是异步操作时,执行顺序为 doA -> doF -> doB -> doC -> doE -> doD // 如果是同步操作时,执行顺序为 doA -> doB -> doC -> doD -> doE -> doF
-
2.3 信任问题
把自己程序的一部分的执行控制交给某个第三方,这被称为控制反转。
而针对自己所写的函数中,同样也需要构建一些防御性的输入参数检查,以便减少或组织无法预料的问题。// foo()是第三方提供的函数,在foo()中回调函数中写的代码的执行控制权,就交给了foo() foo(data, function() { doSomething(); })
// 预期为两个数相加求和 function addNumbers(x, y) { return x + y; // +是可以重载的,通过类型转换,也可以是字符串拼接 } function addNumbers(x, y) { return Number(x) + Number(y); // 确保数字相加 }
-
3 Promise
-
3.1 什么是Promise
从外部看,由于Promise封装了依赖时间的状态,等待底层值的完成或拒绝,所以Promise本身是与时间无关的。因此,Promise可以按照可预测的方式组合,而不用关心时序或底层的结果。
而当Promise决议,它就永远保持在这个状态,此时它就称为了不变值,可以根据需求多次查看,安全地把这个值传递给第三方,并确信它不会被有意无意地修改。
所以,Promise是一种封装和组合未来值的易于复用的机制。 -
3.2 Promise信任问题
-
3.2.1 调用过早
调用过早可能会导致竞态条件。而对一个Promise调用then(…)的时候,即使这个Promise已经决议,提供给then(…)的回调也总会被异步调用,不需要插入setTimeout(…, 0)hack。 -
3.2.2 调用过晚
Promise调用技巧var p = new Promise((resolve, reject) => { resolve(true) }); p.then( function(){ p.then( function(){ console.log( "C" ); } ); console.log( "A" ); } ); p.then( function(){ console.log( "B" ); } ); // A B C
Promise提供了一种称为竞态的高级抽象机制,race(…)var p3 = new Promise(function(resolve, reject) { resolve("B"); }); var p1 = new new Promise(function(resolve, reject) { resolve(p3); }); var p2 = new new Promise(function(resolve, reject) { resolve("A"); }); p1.then(function(data) { console.log(data) }); p2.then(function(data) { console.log(data) }); // A B // 当p1执行时,使用p3决议,按照规则应当把p3展开到p1,但是这是一个异步操作, // 在异步队列中,这个操作排在了p2的回调之后。
function timeoutPromise(delay) { return new Promise(function(resolve, reject) { setTimeout(function() { reject("timeout") }, delay) }) } Promise.race([foo(), timeoutPromise(3000)]) .then(function() { // foo()及时完成 }).catch(err) { // foo()没有完成或者超时 }
-
3.2.4 调用过多
Promise的定义方式使得它只能被决议一次。就算Promise创建代码并试图多次调用resolve(…)或reject(…),或两者同时调用,该Promise也只会接受第一次决议。
且由于Promise只能被决议一次,所哟任何通过then(…)注册的回调都只会调用一次;如果注册了多次,那么它被调用的次数会和注册的次数相同。var p1 = new Promise((resolve, reject) => { resolve('a'); setTimeout(() => { resolve('b') }, 1000); }); p1.then(function(result) { console.log(result) }); p1.then(function(result) { console.log(result) }); // a a
-
3.2.5 未能传递参数/环境值
Promise至多只能有一个决议值(resolve / reject)。
如果没有用任何值显示决议,那么这个值就是undefined。但不管这个值是什么,无论当前或未来,它都会被传给所注册的回调。且只有第一个参数会被传递。var p = Promise.resolve() p.then(result => { console.log(result) }); // undefined
-
3.2.6 吞掉错误或异常
如果Promise 在创建过程中或在查看其决议过程中的任何一个时间点上出现了一个JavaScript异常错误,那么这个异常错误就会被捕获,并且会使这个Promise被拒绝。在回调函数中如果出现异常错误,同样也会被catch,但是前提是有异常捕获回调(catch)。var p = new Promise( function(resolve,reject){ foo.bar(); // foo未定义,所以会出错! resolve( 42 ); // 永远不会到达这里 :( }); p.then(() => { console.log(1) }).catch(err => { console.log(err) }); // ReferenceError: foo is not defined
-
3.2.7 是可信任的Promise吗
如果向Promise.resolve()传递一个非Promise、非thenable的立即值,就会得到一个用这个值填充的promise;而如果传递一个真正的Promise,就只会返回同一个promise;如果传递了一个非Promise的thenable值,promise会试图展开这个值,而且展开过程会持续到提取出一个具体的分类Promise的最终值。
var p = { then: function(cb, errCb) { cb("This is p.then"); errCb("This is p.errCb"); } }; p.then(function(result) { console.log(result); // This is p.then }, function(err) { console.log(err); // This is p.errCb }) Promise.resolve(p) .then(function(result) { console.log(result); }).catch(function(err) { console.log(err) }); // This is p.then
-
-
3.4 链式流
Promise有两个固有行为特征:
1.每次对Promise调用then(…),都会创建并返回一个新的Promise,可以将其链接起来;
2.不管从then(…)调用的完成回调返回的值是什么,都会被自动设置为链接Primise的完成。
使Promise序列真正能够在每一步有异步能力的关键是,Promise.resolve(…)会直接返回接收到的真正Promise,或展开接收到的thenable值,并在持续展开thenable的同时递归地前进。并且从完成/拒绝处理函数返回thenable或者Promise的时候也会发生同样的展开。var p = Promise.resolve(1); p.then(function(val) { return val * 2; }).then(function(result) { console.log(result); // 2 })
Promise中的错误和异常是基于每个Promise的function delay(delay) { return new Promise(function(resolve, reject) { setTimeout(function() { console.log("This step has been delayed " + delay) resolve() }, delay) }) }; delay(1000).then(function () { return delay(1500); }).then(function () { return delay(2000); }).then(function () { delay(3000); }); // This step has been delayed 1000 // This step has been delayed 1500 // This step has been delayed 2000 // This step has been delayed 3000
var p = Promise.resolve(1); var p = Promise.resolve(1); p.then(function(val) { p.then(function(val) { console.log(val); console.log(val); foo().bar(); foo().bar(); }, function(err) { }).then(function(val) { console.log("err1", err) console.log(val); }).then(function(val) { }).catch(function(err) { console.log(val); console.log(err) }, function(err) { }); console.log("err2", err) }) // err1 ReferenceError: foo is not defined // err ReferenceError: foo is not defined
-
3.5 错误处理
当前Promise实例p已经决议,并被1填充,已经不可变。所以在then(…)中抛出的错误只能通知从p.then(…)返回的promise。
var p = Promise.resolve(1); p.then(function(val) { console.log(msg.toLowerCase()); // ReferenceError }, function(err) { console.log("err:", err); // 永远执行不到 })
通过在promise链的末尾添加一个catch(…),捕获链中抛出的错误。但是在catch(…)中的错误和异常至少无法捕获
var p = Promise.resolve(1); p.then(function(val) { console.log(msg.toLowerCase()); // ReferenceError }).catch(function(err) { console.log("err: ", err); console.log(msg2.toLowerCase()); }); // err: ReferenceError: msg is not defined
-
3.6 Promise模式
- 3.6.1 Promise.all([…])
Promise.all([…])需要一个数组参数,通常由Promise实例组成;并且在且仅在所有的成员Promise都完成后,才会完成,如果其中有一个成员被拒绝,主Promise.all([…])就会立即被拒绝;其返回值由所有传入的promise的完成消息组成的数组,与传入的顺序一致(与完成顺序无关)。var p1 = Promise.resolve(1); var p2 = Promise.resolve(2); var p3 = Promise.resolve(3); Promise.all([p1,p2,p3]).then(function(r1,r2,r3) { console.log(r1, r2, r3) }); // [1, 2, 3]
-
3.6.2 Promise.race([…])
Promise中的竞态Promise.race([…]),一旦有任何一个promise决议完成,Promise.race([…])就会完成;一旦有任何一个promise决议拒绝,它就会拒绝;需要传入一个以通常是promise实例组成的数组参数。function randomPromise(count) { count = count || 3; var arr = []; for (var i = 0; i < count; i ++) { var delay = Math.random() * 3 * 1000; // 延时0 - 3s arr.push(new Promise(function(resolve, reject) { setTimeout(function() { delay < 1500 ? resolve(delay) : reject(delay) }, delay) })); } return arr; } Promise.race(randomPromise(5)) .then(function(val) { console.log(val) }).catch(err) { console.log(err); }
-
3.6.3 并发迭代
Promise的异步并发迭代,传入一个由Promise实例组成的数组和对每个实例决议后的值的回调。if (!Promise.map) { Promise.map = function(proArr, cb) { return Promise.all( proArr.map(function(pro) { retrun new Promise(function(resolve) { cb(pro, resolve); }) }) ); } }
- 3.6.1 Promise.all([…])
-
3.7 Promise API概述
-
3.7.1 new Promise(…)构造器
构造器Promise(…)必须和new一起使用,并且必须提供一个函数回调,这个回调是同步的或立即调用的,这个回调函数有两个函数回调resolve(…)和reject(…)。
如果传给resolve(…)的是一个非Promise、非thenable的立即值,就会用这个值完成;如果是一个真正的Promise或thenable值,就会被递归扩展开;并取其最终决议值或状态。 -
3.7.2 Promise.resolve(…)和Promise.reject(…)
创建一个已经完成或已经拒绝的promise实例的快捷方式。var p1 = Promise.resolve(1); // Promise {<resolved>: 1} var p2 = Promise.reject(2); // Promise {<rejected>: 2}
-
3.7.3 then(…)和catch(…)
Promise决议后会立即调用这两个回调函数,但不会两个都调用,而且总是异步调用;并且也会创建并返回一个新的promise,可以用于实现Promise链式流程控制。
then(…)接收两个参数:第一个用来完成回调,第二个用来拒绝回调。如果两者中的任何一个被省略或者作为非函数值传入的话,就会替换为响应的默认回调。默认完成回调只是把消息传递下去,而默认拒绝回调则只是重新抛出其接收到的出错原因。var p = Promise.resolve(1) .then(null, function(err) {}) .then(val => { console.log(val) })
-
3.7.4 Promise.all([…])和Promise.race([…])
这两个静态辅助函数都会创建一个Promise作为它们的返回值。这个promise的决议值完全由传入的promise数组控制。
对Promise.all([…])来说,只有传入的所有promise全部完成,返回的promise才能完成;
对Promise.race([…])来说,只有第一个决议的promise(无论完成还是拒绝),其决议结果会成为返回的promise的决议。
-
-
3.8 Promise的局限性
-
3.8.1 顺序错误处理
Promise链中的错误很容易被无意中默默忽略掉。
如果构建了一个没有错误处理函数的Promise链,那么链中任何地方的任何错误都会在链中一直传递下去,直到被查看。var p = Promise.resolve(1) .then(() => { return Promise.resolve(2) }) console.log(p); // Promise {<resolved>: 2} var p2 = Promise.reject(1) .then(function(val) { }, function(err) { console.log("err1: ", err); }).catch(err => { console.log("err2: ", err); // 不会执行 }); // err1: 1
-
3.8.2 单一值
Promise只能有一个完成值或一个拒绝理由,当要传递多个信息时,会有局限性。
解决:
1.分裂值,即将要传递值分裂成多个promise。function getY(x) { return new Promise( function(resolve,reject){ setTimeout( function(){ resolve( (3 * x) - 1 ); }, 100 ); } ); } function foo(bar, baz) { var x = bar * baz; return [ Promise.resolve(x), getY(x) ]; } Promise.all(foo(10, 20)).then(function(result) { var x = result[0]; var y = result[1]; })
2.展开/传递参数,可以使用ES6的解构赋值
function spread(fn) { return Function.apply.bind(fn, null) } Promise.all( foo(10, 20) ) .then( spread(function(x, y) { console.log(x, y) ; }) ); // ES6 解构赋值 Promise.all(foo(10, 20)) .then(function([x, y]) { console.log(x, y) })
-
3.8.3 单决议
Promise最本质的一个特征是:只能被决议一次(resolve/reject)。在只需异步获取一次值的时候,这可以很好的工作。但当异步请求的另一种模式,一种类似于事件或数据流的情况时,如果不在Promise之上构建显著的抽象,Promise肯定完全不发支持多值决议处理。
var p = new Promise(function(resolve, reject) { click("#btn", resolve); }); p.then(function(evt) { var btnId = evt.currentTarget.id; return request("http://some.url.1/?id=" + btnId); }).then(function(text) { console.log(text) });
在上述例子中,只有当只需要响应按钮点击一次的情况下,这种方式才能工作,如果这个按钮点击了第二次,promise已经决议,因此不会响应第二个resolve。
虽然可以通过针对每次click都启动一个新的promise,但这种需要在事件处理函数中定义整个Promise链,无疑是很丑陋的。 -
3.8.4 惯性
Promise提供了一种不同的范式或者说是编码风格,如果已有大量的基于回调的代码,那么保持现有的编码风格要简单的多。
例如,我们绝对需要一个支持Promise而不是基于回调的Ajax工具,称之为request,我们可以自行实现。但,如果不得不为每个基于回调的工具手工定义支持Promise的封装,这会是一种很大的开销。if (!Promise.wrap) { Promise.wrap = function(fn) { return function() { var args = [].slice.call(arguments); return new Promise(function(resolve, reject) { fn.apply(null, function(err, data) { if (err) reject(err); resolve(data); }) }); } } }
这是一个Promise的封装工具,但是需要传入一个error-first风格的函数,并返回一个创建Promise的函数,替换回调并连接到Promise完成或拒绝。
var request = Promise.wrap(ajax); request("http://some.url.1").then(..)
-
3.8.5 无法取消的Promise
一旦创建了一个Promise并为其注册了完成或拒绝处理函数,如果出现某种情况使得这个任务悬而未决的话,你也没有办法从外部停止它的进程。可以通过侵入式地定义决议回调。
var OK = true; var p = foo(10, 20); Promise.race([p, timeoutPromise(3000).catch(function() { OK = false; throw Error(err) }) ]).then(function(val) { if (OK) { // 如果没有超时 // .. } })
-
3.8.6 Promise的性能
对于基本的基于回调的异步任务链,Promise进行的动作明显要多,也自然意味着会稍慢一些。但这些动作都是为了使Promise能够提供可信任回调及可信任保护支持的列表。
本书作者认为,Promise稍慢一些,但是作为交换,却能得到大量内建的可信任性、避免“异步噩梦”及组合性。这是值得的。并且,Promise解决了我们因只用回调的代码而备受困扰的控制反转问题。Promise并没有摒弃回调,而是将回调的安排转交给了一个位于我们和其他工具之间的可信任的中介机制。
-
-
4 生成器
-
4.1 打破完整运行
ES6引入了一个新的函数类型,其并不符合JavaScript中的一个函数一旦开始执行,就会运行结束的特性。这类函数被称为生成器。var a = 0; function *foo() { a++; yield; // 暂停 console.log(a); } var iter = foo(); // 没有执行生成器*foo(),而是构造了一个迭代器,控制其执行 console.log(a); // 0 iter.next(); // 启动了生成器*foo(),并执行到第一个yield // 调用结束后,*foo()在运行并且是活跃的,但处于暂停状态。 console.log(a); // 1 a++; var result = iter.next(); // 2 // 从暂停恢复了生成器*foo()的执行,并运行到结束。 // result: { value: undefined, done: true }
-
4.1.1 输入和输出
生成器函数虽然具有新的执行模式,但它任然是一个函数,可以接受参数(输入),也能够返回值(输出)。
生成器函数,通过yield和next(…)建立了双向消息传递机制。function *foo(x, y) { return x * y; } var it = foo(2, 3); var result = it.next(); // {value: 6, done: true}
用it接收生成器函数foo()构建的迭代器,并传入参数10,;执行第一个next()时,foo()执行到第一个yield,并通过yield返回值"hello",所以第一个next()返回的结果中的value=“hello”;执行第二个next()时,通过next()传入了值5,这个值作为上一个next()的结果值,与参数x相乘,并返回了y(此时等于50),所以第二个next()返回结果中的value=50,函数执行完毕,done=truefunction *foo(x) { return y = x * (yield "Hello"); } var it = foo(10); it.next(); // { value: "Hello", done: false }; it.next(5); // { value: 50, done: true };
-
4.1.2 多个迭代器
每次构建一个迭代器,实际上就隐式地构建了生成器的一个实例,通过这个迭代器来控制的是这个生成器的实例。function *foo() { var x = yield 2; z++; var y = yield ( x * z ); console.log(x, y, z) } var z = 1; var it1 = foo(), it2 = foo(); var val1 = it1.next().value; // { value: 2, done: false }; var val2 = it2.next().value; // { value: 2, done: false }; val1 = it1.next(val1).value; // { value: 4, done: false } it1.next(val1); // { value: undefined, done, true } x, y, z -> 2 4 2 val2 = it2.next(val2).value; // { value: 6, done: false } it2.next(val2); // { value: undefined, done: true } x, y, z -> 2, 6, 3
-
-
4.2 生成器产生值
-
4.2.1 生产者与迭代器
自定义数字序列生成器实现标准的迭代器接口:var something = (function() { var nextVal; return { [Symbol.iterator]: function() { return this }, next: function() { if (nextVal == undefined) { nextVal = 1; } else { nextVal = nextVal * 3 + 6; } return { value: nextVal, done: nextVal < 10000 ? false : true } } } })(); for (var v of something) { // for..of 要求遍历对象,必须是个迭代器 console.log(v); }
-
4.2.2 iterable(可迭代的)
指一个包含可以在其值上迭代的迭代器的对象。上面例子中的something对象叫做迭代器,因为它的接口中有一个next()方法(迭代器接口)。
从ES6中开始,从一个iterable中提取迭代器的方法是:iterable必须支持一个函数,其名称是专门的ES6
符号值Symbol.iterable。调用这个函数时,返回一个迭代器, -
4.2.3 生成器迭代器
虽然非常类似,但严格来说,生成器本身并不是iterable。当执行一个生成器时,就得到了一个迭代器。
而调用迭代器的return()方法,此时迭代器的done属性被设置为true会立即终止生成器。(for…of循环的“异常结束”(提前终止),通常由break、return或者未捕获异常引起,会向生成器的迭代器发送一个信号使其终止。)function *something() { var nextVal; while (true) { nextVal = nextVal === undefined ? 1 : ( 3 * nextVal ) + 6; yield nextVal } } var it = something(); for ( var v of it ) { console.log( v < 500 ? v : it.return("end").value ) }
-
-
4.3 异步迭代生成器
在yield表达式暂停时,可以通过try…catch语句同步捕获来自那些异步函数调用的错误。function foo(x, y) { ajax("http://some.url.1/?x=" + x + "&y=" + y, function(err, data) { if (err) return it.throw(err); it.next(data); // 将异步获取的数据,传递给暂停的yield表达式 }) } function *main() { try { var text = yield foo(10, 20); console.log(text); } catch(err) { console.log(err); } } var it = main(); it.next(); // 异步获取数据
function *foo() { var x = yield "Hello world"; yield x.toLowerCase(); } var it = foo(); it.next(); try { it.next(111) } catch(err) { console.log(err); // TypeError }
-
4. 生成器+Promise
function foo() { // 返回promise return request("http://some.url.1"); } function *main() { try { var text = yield foo(); console.log(text); } catch (err) { console.error(err); } } var it = main(); var p = it.next().value; p.then(function(text) { it.next(text); }).catch(err) { console.log(err); }
-
4.4.1 支持Promise的Generator Runner
自定义一个独立工具run(),用来以前面展示的方式运行Promise-yeild生成器的工具。function run(gen) { var args = [].slice.call(arguments, 1), it = gen.apply(this, args); return Promise.resolve() .then(function handleNext(val) { var next = it.next(val) ; return (function handleResult(next) { if (next.done) { return next.value } else { Promise.resolve(next.value) .then(handleNext, function handleErr(err) { return Promise.resolve(it.throw(err)).then(handleResult) }) } })(next); }) }; // 使用 function *main() {} run(main);
-
4.4.2 生成器中的Promise并发
生成器中的性能问题
上面的代码中,r1和r2是依次执行的,只有在r1请求完成后才会请求r2。所以性能更高的方案应该是让它们并发执行,即让异步流程基于Promise,特别是基于Promise以时间无关的方式管理状态的能力。functin *foo() { var r1 = yield request("http://some.url.1"), r2 = yield request("http://some.url.2"); var r3 = yield request("http://some.url.3/?r1=" + r1 + "&r2=" + r2); console.log(r3); } run(foo);
function *foo() { var p1 = request("http://some.url.1"), p2 = request("http://some.url.2"); // 也可以使用Promise.all([..]) var r1 = yield p1, r2 = yield p2, r3 = yield request("http://some.url.3/?r1=" + r1 + "&r2=" + r2); console.log(r3) } run(foo)
-
-
4.5 生成器委托
在开发过程中,很可能会出现这种情况,从一个生成器调用另一个生成器。可以使用yield委托,具体语法是:yield* _
在生成器*bar()中,调用foo()创建了一个迭代器,然后yield*把迭代器实例控制(当前*bar()生成器的)委托给了*foo()创建的迭代器。所以,前面两个next()控制的是*bar(),但是第三个next()调用时,foo()启动了,现在控制的是*foo()而不是*bar()。即*bar()把自己的迭代控制委托给了foo()。function *foo() { console.log("*foo() starting"); yield "foo_1"; yield "foo_2"; console.log("*foo() finished"); } function *bar() { console.log("*bar() starting"); yield "bar_1"; yield "bar_2"; yield *foo(); yield "bar_3"; console.log("*bar() finished"); } var it = bar(); it.next(); // "*bar() starting" {value: "bar_1", done: false} it.next(); // {value: "bar_2", done: false} it.next(); // "*foo() starting" {value: "foo_1", done: false} it.next(); // {value: "foo_2", done: false} it.next(); // "*foo() finished" {value: "bar_3", done: false} it.next(); // "*bar() finished" {value: undefined, done: true}
-
4.5.1 为什么要用委托
yield委托的主要目的是代码组织,已达到与普通函数调用的对称。而假设一个生成器函数不仅要单独调用,而且还用在别的生成器中调用的时候,保持生成器的分离有助于程序的可读性,可维护性和可调试性。 -
4.5.2 消息委托
通过yield委托可以实现双向消息流的传递,同样异常也可以被委托。function *foo() { console.log("inside_1 *foo()", yield "foo_A"); console.log("inside_2 *foo()", yield "foo_B"); return "foo_C" } function *bar() { console.log("inside_1 *bar()", yield "bar_A"); console.log("inside_2 *bar()", yield *foo()); console.log("inside_3 *bar()", yield "bar_B"); return "bar_C" } var it = bar(); // 执行到第一个yield暂停,只能输出outside,返回值为第一个yield表达式的返回值bar_A console.log("outside: ", it.next().value); // "outside, bar_A"; // 执行到第二个yield暂停,输出inside_1 *bar,value为传入的参数1; // 此时 yield委托*foo(),*foo()开始启动 // 执行到*foo()中的第一个yield表达式并暂停 // 输出outside,返回值为*foo()中第一个yield表达式的值foo_A // 此时*bar()处于暂停状态 console.log("outside: ", it.next(1).value); // "inside_1 *bar(), 1" // "outside foo_A" // 执行到*foo()中的第二个yield表达式暂停,输出inside_2 *foo(),value为传入的参数2 // 输出outside,返回值为foo_B; // 此时*bar()处于暂停状态 console.log("outside: ", it.next(2).value); // "inside_1 *foo(), 2" // "outside foo_B" // *foo()执行输出inside_2 *foo(),value为3,并返回foo_C // 此时*foo执行结束,将控制权交还给了*bar() // *bar()拥有控制权,执行到第三个yield表达式并暂停,输出inside_2 *bar(),value为foo_C // 输出outside,此时yield表达式的返回值为bar_B console.log("outside: ", it.next(3).value); // "inside_2 *foo() 3" // inside_2 *bar() foo_C" // "outside bar_B" // *bar()执行到最后,输出inside_3 *bar(),value为传入的参数4, // 输出outside,返回值为bar_C console.log("outside: ", it.next(4).value); // "inside_3 *bar(), 4" // "outside bar_C"
-
4.5.3 异步委托
在这个例子中,直接通过yield机制,将在foo()中获取的r3,传递给了*bar()中的变量r3。function *foo() { var r2 = yield request("http://some.url.2"), r3 = yield request("http://some.url.3/?r2=" + r2); return r3; } function *bar() { var r1 = yield request("http://some.url.1"), r3 = yield *foo(); console.log(r3); } run(bar);
-
4.5.4 递归委托
yield委托可以跟踪任意多委托,也可以使用其实现异步的生成器递归,即一个yield委托到它自身的生成器。function *foo(val) { if (val >1) { val = yield *foo(val - 1); } return yield request("http://some.url/?v=" + val); } function *bar() { var r1 = yield *foo(3); console.log(r1); } run(bar);
-