【轉】原文地址:https://www.cnblogs.com/lvdabao/p/es6-promise-1.html
首先感謝原作者呂大豹大神的文章,用大白話講Promise,讓小白比較容易搞懂Promise,這里做個記錄,再貫通一下,也當是自己做筆記了。
先搞明白一個問題,什么是回調函數:
回調函數就是一個通過函數指針調用的函數。如果你把函數的指針(地址)作為參數傳遞給另一個函數,當這個指針被用為調用它所指向的函數時,我們就說這是回調函數。回調函數不是由該函數的實現方直接調用,而是在特定的事件或條件發生時由另外的一方調用的,用于對該事件或條件進行響應
回調函數就是把一個函數fn1()作為一個參數傳遞給另一個函數fn2(),當函數fn2()執行到某一步,或者滿足某種條件的時候才執行傳入的參數fun2()
1. 先看看promise是什么?
console.log('------Promise是什么:-----');
console.dir(Promise);
控制臺查看到如下的結果:
發現
Promise
是一個構造函數,自身有all()
、reject()
、resolve()
這幾個眼熟的方法,prototype
上有then()
、catch()
等同樣很眼熟的方法。所以,用 Promise
new 出來的對象,應該都有上述方法的:
// 實例化一個簡單的 Promise 對象
let p = new Promise(function (resolve, reject) {
// 執行一個異步操作
setTimeout(() => {
console.log('異步操作執行完畢');
resolve('resolve 回調函數的結果');
}, 1000);
});
Promise
對象接收一個函數做為參數,并且該函數參數又有兩個回調函數參數 resolve
和 reject
, resolve
可以暫時理解為異步執行“成功”的回調函數, reject
可以暫時理解為異步執行“失敗”的回調函數;確切的說Promise
對象并沒有“成功”和“失敗”這一說,只是
-
resolve
回調函數將Promise
狀態置為fullfiled
, -
reject
回調函數將Promise
狀態置為rejected
。
運行上面的代碼,會在1秒后輸出一個結果“異步操作執行完畢”;
注意:
- 這里只是實例化了一個對象,并未調用它,但是傳入的匿名函數已經執行了,這點需要注意。所以,一般需要將Promise實例對象封裝一下,在需要的時候,去調用這個封裝的函數即可,對上例進行修改:
function runAsync() { let p = new Promise(function (resolve, reject) { // 執行一個異步操作 setTimeout(() => { console.log('異步操作執行完畢'); resolve('resolve 回調函數的結果'); }, 1000); }); return p; } // 調用封裝的函數 runAsync();// 異步操作執行完畢
-
resolve('resolve 回調函數的結果');
這段代碼并沒有什么效果呢?
這是因為上面說了resolve
是一個回調函數,但是并沒有定義這個函數的邏輯操作resolve('resolve 回調函數的結果');
這段代碼只是給resolve()
函數傳遞了一個“resolve 回調函數的結果”參數而已,但是resolve()
具體邏輯操作是什么,并未定義;帶著這個問題繼續往下進行
2. Promise 能做什么?
在封裝的函數中,最后會 return 一個 Promise 實例,也就是說,執行這個函數我們得到了一個 Promise 實例對象。最開始,在控制臺輸出 Promise 結果可以看到有 then
、 catch
方法的,我們來用一下 then()
方法,then()
方法接受一個參數,這個參數也是函數,為這個函數參數傳入一個參數 params
,然后運行下面的代碼:
runAsync().then(function(params){
console.log(params);
})
運行上面的代碼,會在1秒后輸出如下的結果:
奇跡的發現,
then()
傳入的函數參數不就是 Promise 構造的實例中傳入的那個 resolve
回調函數么,也就是說 resolve()
的執行邏輯是在 then()
傳遞的函數參數中的,它在 runAsync()
這個異步任務執行完之后被執行了。這就是 Promise
的作用之一了:
-
解構回調函數:把原始的回調函數寫法分離出來,寫在
then()
中, 也就是解構了,在異步操作執行完成之后,用鏈式調用的方式執行回調函數
Promise 鏈式調用
從表面上看, Promise 只是能夠簡化層層回調的寫法,而實質上,Promise 的精髓是 “狀態”,用維護狀態、傳遞狀態的方式來使得回調函數能夠及時調用,它相比傳統的callback函數要簡單,靈活。如下面一個場景:
runAsync().then(function (params) {
console.log(params);
return runAsync1();
}).then(function (params) {
console.log(params);
return runAsync2();
}).then(function (param) {
console.log(params);
});
這樣能夠按順序,每隔1秒輸出每個異步回調中的內容,在runAsync2中傳給resolve的數據,能在接下來的then方法中拿到。運行結果如下:
runAsync2()
、runAsync3()
的內部邏輯同 runAsync()
一致,就不再寫了
在 then()
方法中,也可以直接 return 數據,在后面的 then()
中就可以接收到數據了,比如把上面的代碼修改一下:
runAsync().then(function (params) {
console.log(params);
return runAsync1();
}).then(function (params) {
console.log(params);
return params;
}).then(function (param) {
console.log(params);
});
運行后,輸出的結果是:
reject 回調函數和 catch()方法的用法
上面了解了 resolve
回調函數和 then()
的用法,那么還有一個回調函數 reject
沒用呢,這個回調函數又是做什么用的呢?事實上,前面的例子都是只有“執行成功”的回調,但是沒有“執行失敗”的情況,前面說過 reject
回調函數將 Promise
狀態置為 rejected
,這樣我們在then()就能捕捉到,然后執行“失敗”的情況的回調。下面做一個有“失敗”的情況:
function getNum() {
// promise 對象接收一個函數參數,并且該函數參數擁有兩個函數參數,這兩個函數參數是回調函數
let p = new Promise(function (resolve, reject) {
// 執行一個異步操作
setTimeout(() => {
let mathNum = Math.ceil(Math.random() * 10); //生成1-10的隨機數
if (mathNum <= 5) { // 如果數字 ≤ 5,則將promise的狀態值為fullfield,可以理解為“成功了”
console.log('執行完畢 call resolve()...');
resolve('數字小于5'); // 執行 resolve 回調函數
} else { // 如果數字大于5,則將Promise的狀態置為 rejected ,可以理解為“失敗了”
console.log('執行完畢 call reject()...');
reject('數字大于5'); // 執行 reject 回調函數
}
}, 1000);
});
// 返回Promise對象
return p;
}
在最前面,在控制臺輸出 Promise 的時候,會發現,在 then()
方法里面,是可以傳入兩個參數的:
下面調用
getNum
為 then()
方法傳入兩個回調函數參數:
getNum()
.then(function (param) {
console.log(param);
}, function (param) {
console.log(param);
});
執行代碼,會得到以下兩種結果的其中一種:
當隨機數小于等于5的時候,結果如下:
當隨機數大于5的時候,結果如下:
也就說,第二個函數參數執行了
reject
這個回調函數這樣的話,在then() 中也能觸發 reject 這個回調函數,需要作為 then() 方法的第二個參數值傳入
catch的作用
那么
catch()
這個方法是作何使用呢?
- 作用一:指定
reject
的回調
catch
作用之一和then()
方法的第二個參數的作用一樣,用來指定reject
的回調,用法如下:
getNum()
.then(function (param) {
console.log(param);
})
.catch( function (param) {
console.log(param);
});
執行代碼發現同then()方法里面的第二個參數一樣。
- 作用二:在執行resolve的回調(也就是上面then中的第一個參數)時,如果拋出異常了(代碼出錯了),會進入到
catch()
,而不會中斷js的執行
修改上例的代碼:
getNum()
.then(function (param) {
console.log(param);
console.log(param1); //此處的param1 未定義
})
.catch( function (param) {
console.log(param);
});
在 resolve
的回調函數邏輯中,輸出了一個未定義的變量。如果不使用 Promise ,會直接報錯,并中斷js的執行。但是在這里,會得到如下的結果:
也就是拋出異常后進入
catch
了,并且把異常信息作為參數傳遞給了 reject
。即便有錯誤的代碼也不會報錯了。這與try...catch的方法有相同的功能。
3. Promise 的其他用法
在剛開始,控制臺查看 Promise
是什么的時候,發現 Promise
自身還有 race()
和 all()
等方法,下面看看這些方法是做什么用的
1. Promise.all()
的用法
Promise的all方法提供了并行執行異步操作的能力,并且在所有異步操作執行完后才執行回調。使用上面定義好的runAsync、runAsync1、runAsync2這三個函數,看下面的例子:
Promise
.all([runAsync(), runAsync1(), runAsync2()])
.then(function (result) {
console.log(result);
});
all()
方法接受一個數組參數,數組中的值最終都返回Promise對象。這樣,數組中的三個異步操作是并行執行的,等這三個異步操作執行完之后才會進入 then
中,可以將其中一個異步執行的時間長一點,發現最終的返回結果的時間,以所有異步操作執行完畢為準。那么,三個異步操作返回的數據結果去哪兒了呢?同樣的,都在 then 中,all()
方法回把所有的異步操作的結果放進一個數組中作為then回調函數的參數傳入,所以上面代碼輸出的結果:
-
all()
的應用場景:
有了all,就可以并行執行多個異步操作,并且在一個回調中處理所有的返回數據,有一個場景是很適合用這個的,一些游戲類的素材比較多的應用,打開網頁時,預先加載需要用到的各種資源如圖片、flash以及各種靜態文件。所有的都加載完后,我們再進行頁面的初始化。
2. Promise.race()
的用法
上面說了 all()
方法的效果就是“誰最慢,以誰為準進行回調”,那么相對的還有另外一個方法“誰跑的快,以誰為準執行回調”,這就是 race()
方法, race 的用法和 all 一樣,修改下代碼,將三個異步操作的延時時間改為2秒,1秒,3秒,執行race方法:
Promise
.race([runAsync(), runAsync1(), runAsync2()])
.then(function(result){
console.log(result);
});
這三個異步操作也是并行執行的。結果是runAsync()1秒后執行完了,此時then里面的就執行了。看下執行結果:
當執行代碼的時候會發現,跟預期的稍微不太一樣,執行了 runAsync1() 之后,并進入到then()中執行 runAsyac()中的resolve回調函數,輸出了結果,但是 runAsync() 和 runAsync2() 仍在執行,只是沒有執行相應的回調函數,那是因為 then() 已經執行完畢了,所以
race()
方法只能拿到最先執行那個異步操作的結果
-
race()
的應用場景
可以定義一個請求超時的方法,把請求超時的方法和請求都放入race()
中,如果請求的時間超過設定的時間,則直接報請求超時,下面實現一下:
function request() {
return new Promise((resolve, reject) => {
var img = new Image();
img.src = "https://www.cnblogs.com/xxxx";
img.onload = () => resolve(img);
});
}
function timeOut() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('請求超時');
}, 2000);
});
}
Promise
.race([request(), timeOut()])
.then((result) => {
// 處理返回的數據
console.log(result);
$('div').append(result);
})
.catch((reason) => {
console.log(reason);
});
執行代碼,因為圖片的路徑是隨便寫的,肯定返回不了,2秒鐘后結果顯示:請求超時;
如果更換一個正確的圖片路徑,則會正確的顯示結果,并在界面顯示圖片。
結束。