複雜的非同步代碼變得簡單了
OK,現在我們來寫點實際的代碼。假設我們想要: 顯示一個載入指示表徵圖 載入一篇小說的 JSON,包含小說名和每一章內容的 URL。 在頁面中填上小說名 載入所有章節本文 在頁面中添加章節本文 停止載入指示
……這個過程中如果發生什麼錯誤了要通知使用者,並且把載入指示停掉,不然它就會不停轉下去,令人眼暈,或者搞壞介面什麼的。
當然了,你不會用 JavaScript 去這麼繁瑣地顯示一篇文章,直接輸出 HTML 要快得多,不過這個流程是非常典型的 API 請求模式:擷取多個資料,當它們全部完成之後再做一些事情。
首先,搞定從網路載入資料的步驟: 將 Promise 用於 XMLHttpRequest
只要能保持向後相容,現有 API 都會更新以支援 Promise,XMLHttpRequest 是重點考慮對象之一。不過現在我們先來寫個 GET 請求:
function get(url) { // 返回一個新的 Promise return new Promise(function(resolve, reject) { // 經典 XHR 操作 var req = new XMLHttpRequest(); req.open('GET', url); req.onload = function() { // 當發生 404 等狀況的時候調用此函數 // 所以先檢查狀態代碼 if (req.status == 200) { // 以響應文本為結果,完成此 Promise resolve(req.response); } else { // 否則就以狀態代碼為結果否定掉此 Promise // (提供一個有意義的 Error 對象) reject(Error(req.statusText)); } }; // 網路異常的處理方法 req.onerror = function() { reject(Error("Network Error")); }; // 發出請求 req.send(); });}
現在可以調用它了:
get('story.json').then(function(response) { console.log("Success!", response);}, function(error) { console.error("Failed!", error);});
點擊這裡查看代碼。現在我們不需要手敲 XMLHttpRequest 就可以直接發起 HTTP 要求,這樣感覺好多了,能少看一次這個狂駝峰命名的 XMLHttpRequest 我就多快樂一點。 鏈式調用
“then”的故事還沒完,你可以把這些“then”串聯起來修改結果或者添加進行更多非同步作業。 值的處理
你可以對結果做些修改然後返回一個新值:
var promise = new Promise(function(resolve, reject) { resolve(1);});promise.then(function(val) { console.log(val); // 1 return val + 2;}).then(function(val) { console.log(val); // 3});
回到前面的代碼:
get('story.json').then(function(response) { console.log("Success!", response);});
收到的響應是一個純文字的 JSON,我們可以修改 get 函數,設定 responseType為 JSON 來指定伺服器響應格式,也可以在 Promise 的世界裡搞定這個問題:
get('story.json').then(function(response) { return JSON.parse(response);}).then(function(response) { console.log("Yey JSON!", response);});
既然 JSON.parse 只接收一個參數,並返迴轉換後的結果,我們還可以再精簡一下:
get('story.json').then(JSON.parse).then(function(response) { console.log("Yey JSON!", response);});
點擊這裡查看代碼運行頁面,開啟控制台查看輸出結果。 事實上,我們可以把getJSON 函數寫得超級簡單:
function getJSON(url) { return get(url).then(JSON.parse);}
getJSON 會返回一個擷取 JSON 並加以解析的 Promise。 隊列的非同步作業
你也可以把“then”串聯起來依次執行非同步作業。
當你從“then”的回呼函數返回的時候,這裡有點小魔法。如果你返回一個值,它就會被傳給下一個“then”的回調;而如果你返回一個“類 Promise”的對象,則下一個“then”就會等待這個 Promise 明確結束(成功/失敗)才會執行。例如:
getJSON('story.json').then(function(story) { return getJSON(story.chapterUrls[0]);}).then(function(chapter1) { console.log("Got chapter 1!", chapter1);});
這裡我們發起一個對“story.json”的非同步請求,返回給我們更多 URL,然後我們會請求其中的第一個。Promise 開始首次顯現出相較事件回調的優越性了。你甚至可以寫一個抓取章節內容的獨立函數:
var storyPromise;function getChapter(i) { storyPromise = storyPromise || getJSON('story.json'); return storyPromise.then(function(story) { return getJSON(story.chapterUrls[i]); })}// 用起來非常簡單:getChapter(0).then(function(chapter) { console.log(chapter); return getChapter(1);}).then(function(chapter) { console.log(chapter);});
我們一開始並不載入 story.json,直到第一次 getChapter,而以後每次getChapter 的時候都可以重用已經載入完成的 story Promise,所以 story.json 只需要請求一次。Promise 好棒。 錯誤處理
前面已經看到,“then”接受兩個參數,一個處理成功,一個處理失敗(或者說肯定和否定,按 Promise 術語):
get('story.json').then(function(response) { console.log("Success!", response);}, function(error) { console.log("Failed!", error);});
你還可以使用“catch”:
get('story.json').then(function(response) { console.log("Success!", response);}).catch(function(error) { console.log("Failed!", error);});
這裡的 catch 並無任何特殊之處,只是 then(undefined, func) 的文法糖衣,更直觀一點而已。注意上面兩段代碼的行為不僅相同,後者相當於:
get('story.json').then(function(response) { console.log("Success!", response);}).then(undefined, function(error) { console.log("Failed!", error);});
差異不大,但意義非凡。Promise 被否定之後會跳轉到之後第一個配置了否定回調的 then(或 catch,一樣的)。對於 then(func1, func2) 來說,必會調用 func1或 func2 之一,但絕不會兩個都調用。而 then(func1).catch(func2) 這樣,如果 func1 返回否定的話 func2 也會被調用,因為他們是鏈式調用中獨立的兩個步驟。看下面這段代碼: