ES6 異步編程解決方案 之 Promise 對象
一、Promise 概述
Promise 對象是 ES6 提供的原生的內置對象
Promise 是異步編程的一種解決方案(異步代碼同步化),比傳統的解決方案——回調函數和事件——更合理和更強大
- Promise 對象代表一個異步操作, 其不受外界影響,有三種狀態:
- Pending(進行中、未完成的)
- Resolved(已完成,又稱 Fulfilled)
- Rejected(已失敗)
二、Promise 對象的 優缺點
1. 優點
- 異步編程解決方案,避免了回調地獄(Callback Hell)問題
// 回調地獄 firstAsync(function(data){ //處理得到的 data 數據 //.... secondAsync(function(data2){ //處理得到的 data2 數據 //.... thirdAsync(function(data3){ //處理得到的 data3 數據 //.... }); }); });
// 使用 Promise 解決方案
firstAsync()
.then(function(data){
//處理得到的 data 數據
//....
return secondAsync();
})
.then(function(data2){
//處理得到的 data2 數據
//....
return thirdAsync();
})
.then(function(data3){
//處理得到的 data3 數據
//....
});
2. 缺點
- Promise 對象,一旦新建它就會立即執行,無法中途取消
let Promise = new Promise(function(resolve, reject) { console.log(‘Promise‘); resolve(); }); Promise.then(function() { console.log(‘resolved.‘); }); console.log(‘Hi!‘); // Promise // Hi! // resolved
- Promise 狀態一旦改變,就不會再變
const Promise = new Promise(function(resolve, reject) {
resolve(‘ok‘);
throw new Error(‘test‘);
});
Promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// 只輸出 ok
- 當處於Pending狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)
三、Promise 的基本用法
ES6 規定,Promise對象是一個構造函數,用來生成Promise實例
語法:
Promise 構造函數:接收一個函數參數,函數參數 中有兩個參數
resolve
、reject
參數
resolve
、reject
都是函數(由 JavaScript 引擎提供,不用自己部署),在 異步操作成功時,手動執行 函數resolve
;在 異步操作失敗時,手動執行 函數reject
Promise實例生成以後,可以用
then
、catch
方法分別指定 成功狀態、失敗狀態的回調函數
const promise = new Promise(function(resolve, reject) {
// 異步操作 code
// 手動執行 resolve / reject 函數
if (/* 異步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise
.then((value) => {})
.catch((error) => {});
異步編程解決方案: 將異步任務同步操作(一般情況下 Promise 對象 集結合 then、catch 使用)
異步任務:在 創建 Promise 對象中 執行( Promise 對象中的代碼 會立即執行 )
需要在異步任務之後執行的代碼:放到 then、catch 中
四、Promise 原型上的 方法
1. then 方法
作用: 為 Promise 實例添加 狀態改變(成功、失敗) 時的回調函數
- 語法:
Promise.then(resolve, reject)
- 參數:
參數
resolve
:異步操作成功時的回調函數參數
reject
:異步操作失敗時的回調函數(可省略)
- 返回值: 一個新的Promise實例(不是原來那個Promise實例),可采用鏈式寫法
- 參數:
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
// 如上:第一個回調函數完成以後,會將返回結果作為參數,傳入第二個回調函數
- 返回值 有可能是 含有異步操作的 Promise 對象, 這時之後的
then
方法中的函數,就會等待該Promise
對象的狀態發生變化,才會被調用
const getPromise = () => {
const Promise = new Promise(function (resolve, reject) {});
return Promise;
}
getPromise().then(
param => getPromise(param)
).then(
comments => console.log("resolved: ", comments),
err => console.log("rejected: ", err)
);
2. catch 方法
作用: 為 Promise 實例添加 狀態變為
reject
時 的回調函數語法:
Promise.catch(reject)
等同於:
Promise.then(null, reject)
// 示例如下(Promise拋出一個錯誤,就被catch方法指定的回調函數捕獲)
const Promise = new Promise(function(resolve, reject) {
throw new Error(‘test‘);
});
Promise.catch(function(error) {
console.log(error);
});
// Error: test
- Promise 對象的錯誤具有“冒泡”性質: 會一直向後傳遞,直到被捕獲到為止
const getPromise = () => {
const Promise = new Promise(function (resolve, reject) {});
return Promise;
}
getPromise().then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// some code
}).catch(function(error) {
// 處理前面三個Promise產生的錯誤:無論是 getJSON產生錯誤,還是 then 產生的錯誤,都會由 catch 捕捉到
});
- 最佳實踐: 一般來說,不要在then方法裏面定義
reject
狀態的回調函數
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise
.then(function(data) {
// success
})
.catch(function(err) {
// error
});
3. finally 方法
作用: 為 Promise 實例添加 回調函數(不管狀態是
resolve
、reject
),該回調函數都執行語法:
promise.finally(() => {})
不接受任何參數舉例應用:服務器使用 Promise 處理請求,然後使用
finally
方法關掉服務器
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
4. then
、catch
、finally
回調的執行順序
以下原則 適用於:
new Promise(...)
、Promise.resolve(...)
、Promise.reject(...)
回調函數如果是 同步函數: 會在本輪事件循環的末尾執行(下一輪循環之前)
回調函數如果是 異步函數: 會在本輪事件循環的末尾 將 該異步函數放入到事件循環隊列中,等待時間為 0s (也就是在下一輪循環的最後執行)
示例如下:
// 回調函數 是 同步函數
setTimeout(function () {
console.log(3);
}, 0);
const promise = new Promise(function (resolve, reject) {
console.log(6);
resolve();
});
setTimeout(function () {
console.log(4);
}, 0);
promise.then(() => {
console.log(2);
});
setTimeout(function () {
console.log(5);
}, 0);
console.log(1);
// 6 1 2 3 4 5
// 回調函數 是 異步函數
setTimeout(function () {
console.log(3);
}, 0);
const promise = new Promise(function (resolve, reject) {
console.log(6);
resolve();
});
setTimeout(function () {
console.log(4);
}, 0);
promise.then(() => {
setTimeout(() => {
console.log(2);
}, 0);
});
setTimeout(function () {
console.log(5);
}, 0);
console.log(1);
// 6 1 3 4 5 2
5. 回顧:JS 的異常捕獲
作用: 對於可能會出現錯誤的代碼,使用異常捕獲方式,可以使 JS 運行到錯誤代碼處,不終止代碼執行 且 拋出異常錯誤
- 三種最佳實踐:
try...catch
try...finally
try...catch...finally
- 詳解:
try 語句: 包含 可能出現錯誤的代碼
catch 語句: 包含 捕捉到錯誤,執行的代碼
finally 語句: 包含 無論有沒有錯誤,都要執行的代碼
示例:
try {
console.log(11); // 可能會發生錯誤的代碼
}
catch (e) {
throw e; // 拋出錯誤異常,且 catch 代碼塊中 throw 之後的代碼 不會執行
}
finally {
}
五、多個異步 都 執行完成後,再執行回調:Promise.all()
作用: 將多個異步函數 包裝成一個 內部並行執行的 Promise 實例
關鍵: 包裝後的 Promise 實例,內部函數 並行執行;減少了執行時間
- 語法:
const promise = Promise.all([p1, p2, p3])
p1、p2、p3都是 Promise 實例;如果不是Promise 實例,就會先調用
Promise.resolve
方法,將參數轉為 Promise 實例參數可以不是數組,但 必須具有 Iterator 接口,且返回的每個成員都是 Promise 實例
使用場景: 多個異步 都 執行完成後,再執行回調
Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then((results) => {
console.log(results);
});
// 如上:三個異步操作 會 並行執行,等到它們都執行完後,才會執行 then 裏面的代碼;
// 三個異步操作的結果,組成數組 傳給了 then; 也就是 輸出上述 result 為 [異步1數據,異步2數據,異步3數據]
- 包裝後的 Promise 的狀態(區別所在)
只有p1、p2、p3的狀態都變成
fulfilled
,p的狀態才會變成fulfilled
;此時p1、p2、p3的返回值組成一個數組,傳遞給p的回調函數只要p1、p2、p3之中有一個被
rejected
,p的狀態就變成rejected;此時第一個被reject
的實例的返回值,會傳遞給p的回調函數
- Promise 的狀態 決定執行
then
回調,還是執行catch
回調
const p1 = new Promise((resolve, reject) => {
resolve(‘hello‘);
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error(‘報錯了‘);
})
.then(result => result);
Promise.all([p1, p2])
.then((result) => {
console.log(‘then‘);
console.log(result)
})
.catch((e) => {
console.log(‘error‘)
console.log(e);
});
// ‘error‘, Error: 報錯了
const p1 = new Promise((resolve, reject) => {
resolve(‘hello‘);
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error(‘報錯了‘);
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then((result) => {
console.log(‘then‘);
console.log(result)
})
.catch((e) => {
console.log(‘error‘)
console.log(e);
});
// ‘then‘, ["hello", Error: 報錯了]
// 解釋:因為 p2 有自己的 catch, 錯誤才 p2 處被攔截
六、多個異步 中 有一個完成,就執行回調:Promise.race()
作用: 將多個異步函數 包裝成一個 內部並行執行的 Promise 實例
關鍵: 包裝後的 Promise 實例,內部函數 並行執行;減少了執行時間
- 語法:
const promise = Promise.race([p1, p2, p3])
p1、p2、p3都是 Promise 實例;如果不是Promise 實例,(如果是函數)就會先調用下面講到的
Promise.resolve
方法,將參數轉為 Promise 實例參數可以不是數組,但 必須具有 Iterator 接口,且返回的每個成員都是 Promise 實例
使用場景: 多個異步 中 有一個完成,就執行回調
Promise
.race([runAsync1(), runAsync2(), runAsync3()])
.then((results) => {
console.log(results);
});
// 如上:三個異步函數會並行執行,只要有一個執行完,就會執行 then;
// 率先執行完的異步函數的結果 作為參數 傳遞給 then 中的回調函數
// 請求超時的 demo
const p1 = myAjax(‘/resource-that-may-take-a-while‘);
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error(‘request timeout‘)), 5000)
});
Promise.race([p1, p2])
.then(response => console.log(response))
.catch(error => console.log(error));
- 包裝後的 Promise 的狀態(區別所在)
- p1、p2、p3 中 率先 改變狀態的 Promise 的狀態值,直接影響 包裝後的Promise 的狀態;
- 那個率先改變的 Promise 實例的返回值,就傳遞給p的回調函數
六、快速生成 Promise 對象 的方法
1. Promise.resolve()
作用: 將現有對象轉為 Promise 對象(狀態為
resolve
)等價於:
Promise.resolve(‘foo‘);
// 等價於
new Promise((resolve, reject) => {
resolve(‘foo‘);
});
- 參數的幾種形式:
參數是 具有then方法的對象: 轉為 Promise 對象,並立即執行對象的 then 方法 【重點區分】
參數是 Promise 實例: 那麽Promise.resolve將不做任何修改、原封不動地返回這個實例
不帶有任何參數: 最快速生成 Promise 對象
// 參數是 具有then方法的對象
let thenable = {
then: function (resolve, reject) {
console.log(‘then‘);
resolve(42);
}
};
Promise.resolve(thenable); // 輸出 then
// 不帶參數,快速生成 Promise 對象
Promise.resolve();
2. Promise.reject()
作用: 返回一個新的 Promise 實例,該實例的狀態為
rejected
等價於:
Promise.reject(‘出錯了‘);
// 等價於
new Promise((resolve, reject) => {
reject(‘報錯了‘);
});
- 參數: 無論參數是什麽都會原封不動的作為 錯誤理由 【重點區分】
七、Promise 的應用
- Promise 實現 異步加載圖片
// Promise 實現 單張圖片 異步加載(預加載)
function loadImageAsync(url) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = function () {
resolve(image);
};
image.onerror = function () {
reject(new Error(‘Could not load image at ‘ + url));
};
image.src = url;
});
}
loadImageAsync(‘./1png‘)
.then((img) => {
console.log(img); // 圖片標簽
})
// Promise 實現 多張圖片 異步加載(預加載)
function loadImageAsync(url) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = function () {
resolve(image);
};
image.onerror = function () {
reject(new Error(‘Could not load image at ‘ + url));
};
image.src = url;
});
}
function getImages(source) {
const imgs = [];
source.forEach(url => {
loadImageAsync(url)
.then((img) => {
imgs.push(img);
})
});
return imgs;
}
console.log(getImages([‘1.png‘, ‘2.png‘]));
// ES5 實現圖片異步加載
function getAsyncLoadImages(source) {
const imgs = [];
source.forEach(function (url, i) {
imgs[i] = new Image();
imgs[i].onload = function () {
// console.log(imgs[i]);
}
imgs[i].onerror = function () {
reject(new Error(‘Could not load image at ‘ + url));
};
imgs[i].src = url;
})
return imgs;
}
const result = getAsyncLoadImages([‘1.png‘, ‘2.png‘]);
console.log(result);
ES6 異步編程解決方案 之 Promise 對象