我理解的promise

什麼是Promise?

Promise是JS異步編程中的重要概念,異步抽象處理對象,是目前比較流行Javascript異步編程解決方案之一,最早由社區提出並實現,典型的一些庫有Q,when, bluebird等;它的出現是爲了更好地解決JavaScript中異步編程的問題,傳統的異步編程最大的特點就是地獄般的回調嵌套,一旦嵌套次數過多,就很容易使我們的代碼難以理解和維護。而Promise則可以讓我們通過鏈式調用的方法去解決回調嵌套的問題,使我們的代碼更容易理解和維護,而且Promise還增加了許多有用的特性,讓我們處理異步編程得心應手。

Promise,就是一個對象,用來傳遞異步操作的消息。代表了某個未來纔會知道結果的事件(通常是一個異步操作),並且這個事件提供統一的 API,可供進一步處理。

常見異步編程方案

  • 回調函數
  • 事件監聽
  • 發佈/訂閱
  • Promise對象

Promise出現的原因

在Promise出現以前,我們處理一個異步網絡請求通常使用回調函數解決方案,對於回調函數,我們用ajax獲取數據時 都是以回調函數方式獲取的數據,我們來看個例子:

// request 表示 一個異步網絡調用,result1 表示網絡請求的響應。
request1(function(result1){
    處理result1
})

上述代碼看起來好像沒有什麼問題,但是如果我們需要根據第一請求的結果去處理第二個網絡請求,那麼代碼會是下面這個樣子:

request1(function(result1){
    request2(function(result2){
        處理result2
    })
})

上面代碼看起好像還好,但是有可能會出現第二次請求的結果,需要在第三次異步請求時使用,就是多個異步請求之間是依賴關係,那麼我們的代碼就會變成下面這個樣子,即形成回調地獄:

request1(function(result1){
 request2(function(result2){
  request3(function(result3){
   request4(function(result4){
    request5(function(result5){
        ......等等
     })
    })
   })
 })
})

上述代碼不好的是我們基本上還要對每次請求的結果進行處理,代碼會更加臃腫,如果我們在一個團隊中,那麼代碼review以及後續維護將會是一個很困難的過程。

回調地獄帶來的問題
  • 代碼臃腫。
  • 可讀性差。
  • 耦合度過高,可維護性差。
  • 代碼複用性差。
  • 容易產生bug。
  • 只能在回調裏處理異常。

面對上述問題,就有人思考了,能不能用一種更加友好的代碼去解決上述異步嵌套的問題。於是Promise規範誕生了,並且在業界有了很多實現來解決回調地獄的痛點。

Promise處理多個相互關聯的異步請求

Promise可以更直觀的方式來解決"回調地獄"
我們來看個例子:

const promise = url => { 
    return new Promise((resolve, reject) => {
        $.get(url, res => {
            resolve(res)
        });
    })
};
// 請求data1
promise(url).then(res1 => {
    return promise(res1.url);   
}).then(res2 => {
    return promise(res2.url);
}).then(res3 => {
    console.log(res3);
}).catch(err => throw new Error(err));
可以總結爲如下代碼形式:
new Promise(request1)
    .then(request2(result1))
    .then(request3(result2))
    .then(request4(result3))
    .then(request5(result4))
    .catch(處理異常(異常信息))

我們可以比較下這種寫法和上面的回調式的寫法。會發現,Promise 的寫法更爲直觀,簡潔明瞭,並且能夠在外層捕獲異步函數的異常信息。

如何使用promise

ES6給我們提供了一個原生的構造函數Promise,Promise是一個構造函數,我們可以看下這個構造函數:

console.logtypeof Promise();
// "function" 可以看出這是一個構造函數
console.log(Promise);
// function Promise() { [native code] } // ES6的原生支持

new Promise返回一個promise對象接收一個excutor執行函數作爲參數, excutor有兩個函數類型形參resolve reject。

let promise = new Promise((resolve, reject) => {
       // 異步請求處理
       // 異步請求結束後、調用resolve或reject
});

我們對上述代碼解說一下:

  • Promise是一個構造函數,所以使用new操作符來創建promise。
  • 構造函數Promise的參數是一個函數(func),這個函數有兩個參數resolve和reject,分別是兩個函數,作用就是將promise的狀態從pending轉換爲resolved或者從pending轉換爲rejected。
  • 創建後的promise有一些方法,then和catch。當然我們也可人爲的在Promise函數上添加一些滿足自己需求的方法,方便promise對象使用。

promise相當於一個狀態機, 有三種狀態:

  • pending
  • fulfilled
  • rejected
    promise 對象初始化時狀態爲pending,當調用resolve(成功),會由pending => fulfilled, 當調用reject(失敗),會由pending => rejected。 狀態一旦改變,就不會再變。創造promise實例後,它會立即執行。
通俗一點來講就是:

Promise函數體的內部包裹着一個異步的請求或者操作或者函數;然後我們可以在這個異步的操作完成的時候使用resolve函數將我們獲得的結果傳遞出去,或者使用reject函數將錯誤的消息傳遞出去。

注意:promsie狀態只能由pending => fulfilled/rejected, 一旦修改就不能再變。
promise對象方法
  • then方法註冊resolve(成功)/reject(失敗)的回調函數
// onFulfilled接收promise成功的值
// onRejected接收promise失敗的原因
promise.then(onFulfilled, onRejected);
注意:then方法是異步執行的
  • resolve(成功) onFulfilled會被調用
let promise = new Promise((resolve, reject) => {
  resolve('fulfilled'); // 由pending => fulfilled
});
promise.then(result => { // onFulfilled
   console.log(result); // 'fulfilled' 
}, reason => { // onRejected不會被調用
   
})
  • reject(失敗) onRejected會被調用
let promise = new Promise((resolve, reject) => {
  reject('rejected'); // 由pending => rejected
});
promise.then(result => { // onFulfilled 不會被調用
}, (err) => { // onRejected 
   console.log('rejected',err); // 'rejected'
})
  • promise.catch
    在鏈式寫法中可以捕獲上述代碼中發送的異常
promise.catch(onRejected)
相當於
promise.then(null, onRrejected);
// 注意onRejected不能捕獲當前onFulfilled中的異常
promise.then(onFulfilled, onRrejected); 
// 可以寫成:
promise.then(onFulfilled)
       .catch(onRrejected); 
  • promise chain
    promise.then方法每次調用都返回一個新的promise對象所以可以寫成鏈式
    調用形式
function fn1() {console.log("Task 1");}
function fn2() {console.log("Task 2");}
function onRejected(error) {console.log("Catch Error: 1 or 2", error);}
var promise = Promise.resolve();
promise
  .then(fn1())
  .then(fn2())
  .catch(onRejected()) // 捕獲異常
總結:
  • 我們可以看到,then接受兩個函數作爲參數,第一個函數是用來處理resolve的結果,第二個是可選的,用來處理reject的結果。即我們在創建Promise對象的時候,通過函數resolve傳遞出去的結果可以被promise的第一個then方法中的第一個函數捕獲然後作爲它的參數。通過函數reject傳遞出去的結果可以被promise的第一個then方法中的第二個函數捕獲然後作爲它的參數。總結一句話就是(then方法可以接受兩個參數,第一個對應resolve的回調,第二個對應reject的回調)
  • 每一個then方法中都可以再次新創建一個Promise對象,然後返還給下一個then方法處理。
  • resolve找then裏的成功回調,reject找then裏失敗的回調。
Promise的靜態方法
  • Promise.resolve返回一個fulfilled狀態的promise對象
Promise.resolve('node').then(function(res){
   console.log(res); // node
});
Promise.resolve('node'); // node
// 相當於
let promise = new Promise(resolve => {
  resolve('node'); // node
});
  • Promise.reject返回一個rejected狀態的promise對象
Promise.reject(‘失敗’);
new Promise((resolve, reject) => {
   reject(‘失敗’);
});
  • Promise.all接收一個promise對象數組爲參數
    只有全部爲resolve纔會調用,通常會用來處理多個並行異步操作
    誰跑的慢,以誰爲準執行回調。all接收一個數組參數,裏面的值最終都返回Promise對象。
    有了all,就可以並行執行多個異步操作,並且在一個回調中處理所有的返回數據,是不是很爽?有一個場景是很適合用這個,比如一些遊戲類的素材比較多的應用,打開網頁時,預先加載需要用到的各種資源如圖片、flash以及各種靜態文件。所有的都加載完後,我們再進行頁面的初始化。
let p1 = new Promise((resolve, reject) => {
   resolve(1);
});
let p2 = new Promise((resolve, reject) => {
   resolve(2);
});
let p3 = new Promise((resolve, reject) => {
   resolve(3);
});
Promise.all([p1, p2, p3]).then(data => { 
   console.log(data); // [1, 2, 3] 結果順序和promise實例數組順序是一致的
}, err => {
   console.log(err);
});
  • Promise.race 接收一個promise對象數組爲參數
    Promise.race 只要有一個promise對象進入FulFilled或者Rejected 狀態的話,就會繼續進行後面的處理。
    誰跑的快,以誰爲準執行回調
function timerPromisefy(delay) {
   return new Promise(function (resolve, reject) {
       setTimeout(function () {
           resolve(delay);
       }, delay);
   });
}
var startDate = Date.now();
Promise.race([
   timerPromisefy(10),
   timerPromisefy(20),
   timerPromisefy(30)
]).then(function (values) {
   console.log(values); // 10
});

Promise特點

  • Promise的立即執行性
const promise = new Promise(function(resolve, reject){
 console.log("create a promise");
 resolve("success");
});
console.log("after new Promise");
promise.then(function(res){
 console.log(res);
});
輸出: // "create a promise"
// "after new Promise"
// "success"

Promise對象表示未來某個將要發生的事件,但在創建(new)Promise時,作爲Promise參數傳入的函數是會被立即執行的,只是其中回調執行的代碼是異步代碼。

  • Promise 狀態的不可逆性
const promise1 = new Promise(function(resolve, reject){
  resolve("success1");
  resolve("success2");
});
const promise2 = new Promise(function(resolve, reject){
  resolve("success");
  reject("reject");
});
promise1.then(function(res){
  console.log(res);
});
promise2.then(function(res){
  console.log(res);
});
// 輸出結果: "success1" "success"
  • 鏈式調用
var p = new Promise(function(resolve, reject){
 resolve(1);
});
p.then(function(value){               //1
 console.log(value);
 return value*2;
}).then(function(value){              //2
 console.log(value);
}).then(function(value){              //3
 console.log(value);
 return Promise.resolve('resolve'); 
}).then(function(value){              //4
 console.log(value);
 return Promise.reject('reject');
}).then(function(value){              //5
 console.log('resolve: '+ value);
}, function(err){
 console.log('reject: ' + err);
})
// 輸出:
1
2
undefined
"resolve"
"reject: reject"

Promsie 與事件循環

Promise在初始化時,傳入的函數是同步執行的,然後註冊then回調,註冊完之後,繼續往下執行同步代碼,在這之前,then 中回調不會執行。同步代碼塊執行完畢後,纔會在事件循環中檢測是否有可用的 promise 回調,如果有,那麼執行,如果沒有,繼續下一個事件循環。

promise應用場景

  • 回調地獄,代碼難以維護, 通常第一個的異步請求的輸出是第二個請求的輸入這種現象(多個請求互相依賴關係)。
  • promise可以支持多個併發的請求,獲取併發請求中的數據
  • promise可以解決異步的問題,本身不能說promise是異步的

promise使用總結

  • 首先初始化一個 Promise 對象,可以通過兩種方式創建, 這兩種方式都會返回一個 Promise 對象。
    1、new Promise(fn)
    2、Promise.resolve(fn)
  • 然後調用上一步返回的 promise 對象的 then 方法,註冊回調函數。
    1、then 中的回調函數可以有一個參數,也可以不帶參數。如果 then 中的回調函數依賴上一步的返回結果,那麼要帶上參數。比如
    new Promise(fn)
    .then(fn1(value){
        //處理value
    })
  • 最後註冊catch異常處理函數,處理前面回調中可能拋出的異常。
    通常通過這三個步驟,就能夠應對絕大部分的異步處理場景。

最後我們看一些常見的promise例子:

demo1:

const p1 = new Promise((resolve, reject) => {
  console.log(1)
  resolve()
  console.log(2)
})
p1.then(() => {
  console.log(3)
})
console.log(4)

最終輸出結果是:1 2 4 3,Promise 構造函數是同步執行的,promise.then 中的函數是異步執行的
demo2:

const p1 = new Promise((resolve, reject) => {
  resolve('success1')
  reject('error')
  resolve('success2')
})
p1.then((res) => {
    console.log('then: ', res)
  })
  .catch((err) => {
    console.log('catch: ', err)
  })

最終輸出結果:success1,構造函數中的resolve或reject只有第一次執行有效,多次調用沒有任何作用,即promise 狀態一旦改變則不能再變。
後續會有一篇文章講解下如何手寫一個promise。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章