javascript忍者祕籍-第六章 生成器與promise

6.1 生成器

生成器時一種特殊類型的函數javascript

當從頭至尾運行標準函數時,最多隻生成一個值。html

而生成器函數會在幾回運行請求中暫停,所以每次運行均可能生成一個值java

//普通獲取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 使用生成器函數

生成器函數是一個全新的函數類型,能生成一組值的序列,但每一個值的生成是基於每次請求,而且不一樣於標準函數的當即生成。咱們必須顯式的向生成器請求一個新的值,隨後生成器要麼相應一個新生成的值,要麼不會再生成新值node

生成器幾乎從不掛起,當對另外一個值的請求到來後,生成器就會從上次離開的位置恢復執行。json

function* WeaponGenerator(){
    yield "Katana";
    yield "Wakizashi";
    yield "Kusarigama";
}
for(let weapon of WeaponGenerator()){
    assert(weapon !=== undefined,weapon);  //分三次輸出
}
複製代碼

調用生成器不會執行生成器函數,相反,它會建立一個叫迭代器的對象(iterator)。數組

經過迭代器對象控制生成器

調用生成器函數 不必定會執行 生成器函數體.會建立一個迭代器。經過建立迭代器對象,能夠與生成器通訊promise

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();app

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表達式(生成器當前掛起的表達式)的值。

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)狀態,在該狀態下咱們可以成功獲取到承諾的值

image-20181103093342369

若是 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 的過程當中拋出一個異常
//1、顯示拒絕
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"));
複製代碼
//2、隱式拒絕
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相結合咱們可以使用 同步代碼 來簡化 異步任務

相關文章
相關標籤/搜索