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相結合我們能夠使用 同步程式碼 來簡化 非同步任務