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

smiler2018發表於2018-12-27

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

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 的過程中丟擲一個異常
//一、顯示拒絕
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相結合我們能夠使用 同步程式碼 來簡化 非同步任務

相關文章