6.1 生成器
生成器时一种特殊类型的函数
当从头到尾运行标准函数时,最多只生成一个值。
而生成器函数会在几次运行请求中暂停,因此每次运行都可能生成一个值
//普通获取JSON数据 异步 太耗时了
try{
var ninjas = syncGetJSON("ninjas.json");
var missions = syncGetJSON(ninjas[0].missionsUrl);
var missionDetails = syncGetJSON(missions[0].detailsUrl);
}catch(e){}
//回调解决 嵌套地狱
getJSON("ninjas.json",function(err,ninjas){
if(err){
//...
}
getJSON(ninjas[0].missionsUrl,function(err,missions){
if(err){
//...
}
getJSON(missions[0].detailsUrl,function(err,missionDetails){
if(err){
//...
}
//Study the intel plan
})
})
})
//生成器
//在 function 关键字后增加一个 *号 可以定义生成器函数.在生成器函数中可使用新的 yield 关键字
async(function* (){
try{
const ninjas = yield getJSON('ninjas.json');
const missions = yield getJSON(ninjas[0].missionsUrl);
const missionDescription = yield getJSON(missions[0].detailsUrl);
//Study the mission details
}catch(e){
//....
}
})
复制代码
6.2 使用生成器函数
生成器函数是一个全新的函数类型,能生成一组值的序列,但每个值的生成是基于每次请求,并且不同于标准函数的立即生成。我们必须显式的向生成器请求一个新的值,随后生成器要么相应一个新生成的值,要么不会再生成新值
生成器几乎从不挂起,当对另一个值的请求到来后,生成器就会从上次离开的位置恢复执行。
function* WeaponGenerator(){
yield "Katana";
yield "Wakizashi";
yield "Kusarigama";
}
for(let weapon of WeaponGenerator()){
assert(weapon !=== undefined,weapon); //分三次输出
}
复制代码
调用生成器不会执行生成器函数,相反,它会创建一个叫迭代器的对象(iterator)。
通过迭代器对象控制生成器
调用生成器函数 不一定会执行 生成器函数体.会创建一个迭代器。通过创建迭代器对象,可以与生成器通信
function* WeaponGenerator(){
yield "Katana";
yield "Wakizashi";
}
const weaponsIterator = WeaponGenerator(); //创建一个迭代器,来控制生成器的执行
const result1 = weaponsIterator.next();
result1 //结果为一个对象
result1.value; //"Katana" 包含一个返回值
result1.done; //还包含一个指示器 告诉我们生成器是否还会生成值
//...
const result3 = weaponsIterator.next();
result3.value; //"undefined"
result3.done; //true 已完成
复制代码
迭代器用于控制生成器的执行。迭代器对象暴露的最基本接口是 next 方法,这个方法可以用来向生成器请求一个值,从而控制生成器:
const result1 = weaponsIterator.next();
next 函数调用后,生成器就开始执行代码,当代码执行到 yield 关键字时,就会生成一个中间结果(生成值序列中的一项),然后返回一个新对象,其中封装了结果值和一个指示完成的指示器。
每当生成一个当前值后,生成器就会非阻塞的挂起执行,随后耐心等待下一次值请求的到达。这是普通函数完全不具有的强大特性。
对迭代器进行迭代
//while循环迭代生成器
function* WeaponGenerator(){
yield "Katana";
yield "Wakizashi";
}
const weaponsIterator = WeaponGenerator(); //迭代器
let item;
while(!(item = weaponsIterator.next()).done){
item.value; //值
}
//for-of循环是对迭代器进行迭代的语法糖
for(var item of WeaponGenerator()){
item; //值
}
复制代码
把执行权交给下一个生成器
可以在标准函数中调用另一个标准函数 => 可以把生成器的执行委托给另一个生成器
//使用 yield 操作符将执行权交给另一个生成器
//在迭代器上使用 yield* 操作符,程序会跳转到另外一个生成器上执行
function* WarriorGenerator(){
yield "Sun Tzu";
yield* NinjaGenerator(); //yield* 将执行权交给了另一个生成器
yield "Genghis Khan";
}
function* NinjaGenerator(){
yield "Hattori";
yield "Yoshi";
}
//for-of 循环不会关心 WarriorGenerator 委托到另一个生成器上,只关心 done 状态到来之前都一直调用 next 方法
for(let warrior of WarriorGenerator()){
warrior; //都有
}
复制代码
使用生成器
用生成器生成 ID 序列
//使用生成器生成唯一ID序列
function* IdGenerator(){
let id = 0; //一个始终记录ID的变量,这个变量无法在生成器外部改变
while(true){ //循环生成无限长度的ID序列
yield ++id;
}
}
const idIterator = IdGenerator();
idIterator.next().value; //1
idIterator.next().value; //2
复制代码
使用迭代器遍历 DOM 树
<div id="subTree">
<form>
<input type="text" />
</form>
<p>Paragraph</p>
<span>Span</span>
</div>
复制代码
//递归函数
function traverseDOM(element,callback){
callback(element);
element = element.firstElementChild;
while(element){
traverseDOM(element,callback);
element = element.nextElementSibling;
}
}
const subTree = document.getElementById("subTree");
traverseDOM(subTree,function(element){
assert(element !== null,element.nodeName);
})
复制代码
//用生成器遍历 DOM 树
function* DomTraversal(element){
yield element;
element = element.firstElementChild;
while(element){
yield* DomTraversal(element); //用 yield* 将迭代控制转移到另一个DomTraversal生成器实例上
element = element.nextElementSibing;
}
}
const subTree = document.getElementById('subTree');
for(let element of DomTraversal(subTree)){
assert(element !== null,element.nodeName);
}
复制代码
告诉我们不必使用回调函数的情况下,使用生成器函数来解耦代码,从而将生产值(HTML节点)的代码和消费值(for-of循环打印、访问过的节点)的代码分隔开。迭代器比递归自然,保持一个开放的思路很重要。
与生成器交互
作为生成器函数参数发送值
向生成器发送值的最简单方法是:调用函数并传入实参
//向生成器发送数据及从生成器接收数据
//生成器可以像其他函数一样接收 标准参数
function* NinjaGenerator(action){
const imposter = yield ("Hattori " + action);
//传回的值将作为yield表达式的返回值,因此impostrer的值是Hanzo
assert(imposter === "Hanzo","The generator has been infiltrated")
yield ("Yoshi (" + imposter + ") " + action)
}
const ninjaIterator = NinjaGenerator("skulk");
const result1 = ninjaIterator.next();
assert(result1.value === "Hattori skulk","Hattori is skulking");
const result2 = ninjaIterator.next("Hanzo");
assert(result2.value === "Yoshi(Hanzo)skulk","We have an imposter!")
复制代码
使用 next 方法向生成器发送值
除了第一次调用生成器的时候向生成器提供数据,我们还能通过 next 方法向生成器传入参数。这个过程中,我们把生成器函数从挂起状态恢复到了执行状态。
生成器把这个传入的值用于整个yield表达式(生成器当前挂起的表达式)的值。
next() 方法为等待的 yield 表达式提供了值,所以,如果没有等待中的 yield 表达式,也就没有什么值能应用的。
基于此,我们无法通过第一次调用 next 方法向生成器提供该值。但是,如果你需要为生成器提供一个初始值,你可以调用生成器自身,就像 NinjaGenerator("skulk")
function* Gen(val){
val = yield val * 2;
yield val;
}
let generator = Gen(2);
let a1 = generator.next(3).value; //4
let a2 = generator.next(5).value; //5
复制代码
抛出异常
每个迭代器除了一个 next 方法,还有一个 throw 方法
//向生成器抛出异常
function* NinjaGenerator(){
try{
yield "Hattori";
fail("The expected exception didn't occur");
}catch(e){
assert(e === "Catch this!","Aha! We caught an exception");
}
}
const ninjaIterator = NinjaGenerator();
const result1 = ninjaIterator.next();
assert(result1.value === "Hattori","We got Hattori");
ninjaIterator.throw("Catch this!"); //向生成器抛出一个异常
//可以用来改善异步服务器通信
复制代码
探索生成器内部构成
调用一个生成器不会实际执行它。它会创建一个新的 迭代器 ,通过该迭代器我们才能从生成器中请求值。在生成器生成(让渡)了一个值后,生成器会挂起执行并等待下一个请求的到来。
- 挂起开始 — 创建了一个生成器后,它最先以这种状态开始。其中的任何代码都未执行
- 执行 — 生成器中的代码执行的状态。执行要么是刚开始,要么是从上次挂起的时候继续的。当生成器对应的迭代器调用了 next 方法,并且当前存在可执行的代码时,生成器都会转移到这个状态。
- 挂起让渡 — 当生成器在执行过程中遇到了一个 yield 表达式,它会创建一个包含着返回值的新对象,随后再挂起执行。生成器在这个状态暂停并等待继续执行
- 完成 — 在生成器执行期间,如果代码执行到 return 语句或者全部代码执行完毕,生成器就进入该状态
生成器时如何跟随执行环境上下的呢?看下图:
当我们从生成器中取得控制权后,生成器的执行环境上下文一直是保存的,而不是像标准函数一样退出后销毁。
6.3 使用 promise
promise对象是对我们现在尚未得到但将来会得到值的占位符
const ninjaPromise = new Promise((resolve,reject) => { //传入两个函数参数
resolve("Hattori");
});
ninjaPromise.then(ninja => {
assert(ninja === "Hattori","We were promised Hattori!");
},err => {
fail("There shouldn't be an error");
})
复制代码
用新的内置构造函数 Promise 创建一个 promise 需要传入一个函数,这个函数被称为 执行函数 ,它包含两个参数 resolve 和 reject。当把这两个内置函数:resolve 和 reject 作为参数传入 Promise 构造函数后,执行函数会立刻调用。
代码调用 Promise 对象内置的 then 方法,我们向这个方法中传入两个回调函数:一个成功回调函数和一个失败回调函数。当承诺成功兑现(在 promise 上调用 resolve),前一个回调就会被调用,而当出现错误就会调用后一个回调函数(可以是发生了一个未处理的异常,也可以是在 promise 上调用了 reject)
回调函数的三个问题:
- 错误难以处理
- 执行连续步骤非常棘手
- 执行很多并行任务也很棘手
深入研究 promise
promise对象用于作为 异步任务结果的占位符。它代表了 一个我们暂时还没获得但在未来有望获得的值。
在一个 promise 对象的整个生命周期中,它会经历多种状态,如图 6.10 所示。一个 promise 对象从等待(pending)状态开始,此时我们对承诺的值一无所知。因此一个等待状态的 promise 对象也称为未实现(unresolved)的 promise。在程序执行的过程中,如果 promise 的 resolve 函数被调用,promise 就会进入完成(fulfilled)状态,在该状态下我们能够成功获取到承诺的值
如果 promise 的 reject 函数被调用,或者如果一个未处理的异常在 promise 调用的过程中发生了,promise 就会进入到拒绝状态,尽管在该状态下我们无法获取承诺的值,但我们至少知道了原因。一旦某个 promise 进入到完成态或者拒绝态,它的状态都不能再切换了(一个 promise 对象无法从完成态再进入拒绝态或者相反)。
//promise的执行顺序
report('At code start');
var ninjaDelayedPromise = new Promise((resolve, reject) => {
report('ninjaDelayedPromise executor');
setTimeout(() => {
report('Resolving ninjaDelayedPromise');
resolve('Hattori');
}, 500);
});
console.log(ninjaDelayedPromise);
assert(ninjaDelayedPromise !== null, 'After creating ninjaDelayedPromise');
ninjaDelayedPromise.then(ninja => {
assert(
ninja === 'Hattori',
'ninjaDelayedPromise resolve handled with Hattori'
);
});
const ninjaImmediatePromise = new Promise((resolve, reject) => {
report('ninjaImmediatePromise executor.Immediate resolve.');
resolve('Yoshi');
});
ninjaImmediatePromise.then(ninja => {
assert(ninja === 'Yoshi', 'ninjaImmediatePromise resolve handled with Yoshi');
});
report('At code end');
//结果如下
At code start
ninjaDelayedPromise executor
Promise { <pending> }
After creating ninjaDelayedPromise
ninjaImmediatePromise executor.Immediate resolve.
At code end
ninjaImmediatePromise resolve handled with Yoshi
Resolving ninjaDelayedPromise
ninjaDelayedPromise resolve handled with Hattori
复制代码
Promise 是设计用来处理异步任务的。JavaScript 通过本次事件循环中的所有代码都执行完毕后,调用 then 回调函数来处理 promise
拒绝promise
- 显示拒绝:在一个 promise 的执行函数中调用传入的reject方法
- 隐式拒绝:正处理一个 promise 的过程中抛出一个异常
//一、显示拒绝
const promise = new Promise((resolve,reject) => {
reject("Explicitly reject a promise");
})
//1.如果promise被拒绝,第二个回调函数error总是被调用
promise.then(
() => fail("Happy path,won't be called!"),
error => pass("A promise was explicitly rejected!")
)
//2.用catch处理拒绝
promise.then(
() => fail("Happy path,won't be called!")
).catch(() => pass("Promise was also rejected"));
复制代码
//二、隐式拒绝
const promise = new Promise((resolve,reject) => {
undeclaredVariable++; //未定义 抛出错误
});
promise.then(()=>fail("Happy path,won't be called!"))
.catch(error => pass("Third promise was alse rejected"));
复制代码
真实promise案例
function getJSON(url){
return new Promise((resolve,reject) => {
const request = new XMLHttpRequest();
request.open("GET",url);
request.onload = function(){
try{
if(this.status === 200){
resolve(JSON.parse(this.response)); //无效的JSON代码
}else{ //服务器返回错误
reject(this.status + " " + this.statusText);
}
}catch(e){
reject(e.message);
}
}
//通信中发生错误
request.onerror = function(){
reject(this.status + " " + this.statusText)
}
request.send();
})
}
//3个潜在的错误源:客户端和服务器之间的连接错误、服务器返回错误的数据(无效的响应状态码)、无效的JSON代码
getJSON("data/ninjas.json").then(ninjas => {
assert(ninjas !== null,"Ninjas obtained!");
}).catch(e => fail("Shouldn't be here:" + e));
复制代码
链式调用 promise
我们可以在 then 函数上注册一个回调函数,一旦 promise 成功兑现就触发该回调函数
调用 then 方法后还可以再返回一个新的 promise 对象
//链式调用 promise
getJSON('data/ninjas.json')
.then(ninjas => getJSON(ninjas[0].missionsUrl))
.then(missions => getJSON(missions[0].detailsUrl))
.then(mission => assert(mission !== null,'Ninja mission obtained!'))
.catch(error => fail('An error has occurred'))
复制代码
Promise 链中的错误捕获
...catch(error => fail("An error has occurred:" + err));
复制代码
如果错误在前边的任何一个 promise 中产生,catch 方法都会捕捉到,统一处理。
等待多个promise
Promise.all([
getJSON("data/ninjas.json"),
getJSON("data/mapInfo.json"),
getJSON("data/plan.json")
]).then(results => {
const ninjas = results[0],mapInfo = results[1],plan = results[2];
//...
}).catch(error => {
fail("A problem in carrying out our plan!");
})
复制代码
通过内置方法 Promise.all
可以等待多个 promise。这个方法将一个 promise 数组作为参数,然后创建一个新的 promise 对象,一旦数组中的 promise 全部被解决,这个返回的 promise 就会被解决,一旦其中一个 promise 失败了,那么整个新 promise 对象也会被拒绝。
后续的回调函数接收成功值组成的数组,数组中的每一项都对应 promise 数组中的对应项。
promise 竞赛
Promise.race([
getJSON("data/yoshi.json"),
getJSON("data/hattori.json"),
getJSON("data/hanzo.json")
]).then(ninja => {
//...
}).catch(error => fail("Failure!"));
复制代码
使用 Promise.race 方法传入一个 promise 数组会返回一个全新的 promise 对象,一旦数组中某一个 promise 被处理或被拒绝,这个返回的 promise 就同样会被处理或被拒绝。
6.4 把生成器和promise结合
比较代码
以下分别以 同步 和 异步 的代码来书写
同步
try {
const ninjas = syncGetJSON('data/ninjas.json');
const missions = syncGetJSON('ninjas[0].missionsUrl');
const missionDetails = syncGetJSON(missions[0].detailsUrl);
} catch (e) {
//
}
复制代码
缺点是: UI 被阻塞了
解决方案:将 生成器 和 promise 相结合
从生成器中让渡后会挂起执行而不会发生阻塞.仅需调用生成器迭代器的next方法,就可以唤醒生成器并继续执行.而promise在未来触发某种条件的情况下得到允诺的值,发生错误时执行相应的回调函数.
异步
自定义函数 promise与生成器结合
//将 promise 和 生成器结合
function async(generator) {
var iterator = generator();
function handle(iteratorResult) {
if (iteratorResult.done) {
return;
}
const iteratorValue = iteratorResult.value;
if (iteratorValue instanceof Promise) {
iteratorValue
.then(res => handle(iterator.next(res)))
.catch(err => iterator.throw(err));
}
}
try {
handle(iterator.next());
} catch (error) {
iterator.throw(error);
}
}
async(function*() {
try {
const ninjas = yield getJSON('data/ninjas.json');
const missions = yield getJSON(ninjas[0].missionsUrl);
const missionDescription = yield getJSON(missions[0].detailUrl);
} catch (error) {}
});
复制代码
//用异步的方式写同步代码
//之前代码
getJSON('data/ninjas.json',(err,ninjas) => {
if(err){...}
getJSON(ninjas[0].missionsUrl,(err,missions) => {
if(err){...}
console.log(missions);
})
})
//async
async(function*(){
try{
const ninjas = yield getJSON('data/ninjas.json');
const missions = yield getJSON(ninjas[0].missionsUrl);
}catch(e){
//error
}
})
复制代码
面向未来的async函数
在关键字 function 之前使用关键字 async,表明当前的函数依赖一个异步返回的值。在每个异步任务的位置上,都要放置一个 await 关键字,用来告诉 JavaScript 引擎,请在不阻塞应用执行的情况下在这个位置上等待执行结果
(async function() {
try {
const ninjas = await getJSON('data/ninjas.json');
const missions = await getJSON(ninjas[0].missionsUrl);
console.log(missions);
} catch (error) {
console.log('Error:', error);
}
})();
复制代码
总结
同步代码让我们更容易理解、使用标准控制流以及异常处理机制、try-catch语句的能力。
异步代码有天生的非阻塞,当等待长时间运行的异步任务时,应用的执行不应该被阻塞。
通过将生成器和promise相结合我们能够使用 同步代码 来简化 异步任务