javascript忍者秘籍-第六章 生成器与promise

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)

回调函数的三个问题:

  1. 错误难以处理
  2. 执行连续步骤非常棘手
  3. 执行很多并行任务也很棘手

深入研究 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

  1. 显示拒绝:在一个 promise 的执行函数中调用传入的reject方法
  2. 隐式拒绝:正处理一个 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相结合我们能够使用 同步代码 来简化 异步任务

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值