Promise實用理解

cheche062發表於2018-03-21

1.Promise含義

Promise 是非同步程式設計的一種解決方案優於傳統的解決方案——回撥函式和事件。 簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。 Promise物件代表一個非同步操作,有三種狀態:pending(進行中)、resolved(已成功)和rejected(已失敗)。

2.Promise執行順序

setTimeout(() => {
    console.log('a');
}, 0);
let p = new Promise((resolve, reject) => {
    console.log('b');
    resolve();
});
p.then(() => {
    console.log('d');
});
console.log('c');

// 控制檯輸出:
// 'b'
// 'c'
// 'd'
// 'a'

複製程式碼

要理解該輸出順序首先應該瞭解js的執行任務佇列優先順序(由高到低)

  • 主執行緒
  • Micro-task佇列 (微任務)
  • Macro-tasks佇列 (巨集任務)

首先setTimeout屬於巨集任務扔進Macro-tasks佇列,新建例項Promise時接受一個回撥函式作為引數,注意此時該回撥函式屬於主執行緒會立刻執行,輸出'b'緊接著執行resolve也就意味著該promise物件的狀態將從pending更新為resolved,其掛載的回撥函式也就是then裡面的引數函式並不會立即執行,因為它屬於微任務,所以丟進Micro-task佇列。接下來輸出'c',到目前為止主執行緒任務已經結束,接著執行微任務輸出'd',最後執行巨集任務輸出'a'

3.Promise狀態更新

let p1 = new Promise(function (resolve, reject) { 
    resolve('p1');
});
let p2 = new Promise(function (resolve, reject) {
    setTimeout(() => {
        resolve('p2')
    }, 100);
});
let p3 = new Promise(function (resolve, reject) {
    setTimeout(() => {
        reject('p3')
        resolve('p3')
    }, 100);
});

p1.then((value) => {
    console.log(value);
})
p2.then((value) => {
    console.log(value);
})
p3.then((value) => {
    console.log('success', value);
}, (value) => {
    console.log('error', value);
})
console.log('p1:', p1);
console.log('p2:', p2);
console.log('p3:', p3);

setTimeout(() => {
    console.log('p1:', p1);
    console.log('p2:', p2);
    console.log('p3:', p3);
}, 100);

// 控制檯輸出
// p1: Promise {[[resolved]]: "p1"}
// p2: Promise {[[pending]]}
// p3: Promise {[[pending]]}
// p1
// p2
// error p3
// p1: Promise {[[resolved]]: "p1"}
// p2: Promise {[[resolved]]: "p2"}
// p3: Promise {[[rejected]]: "p3"}
複製程式碼

p1最新建立就呼叫了resolve則它的狀態立刻變為resolved,值為p1,但此時p2和p3都為pending狀態,100毫秒後p2輸出值p2且狀態轉為resolved。 p3首先呼叫了reject則其狀態轉為rejected,值為p3,儘管下一行又呼叫了resolve但並沒有任何作用忽略成功的回撥,只有error p3。 這段實驗也顯示出Promise的一個特點

  • 呼叫then方法傳入回撥可以從外部接受promise的非同步返回資料value,當巢狀多級非同步操作時這種優勢更大。
  • 狀態的不可逆性,Promise的狀態和值確定下來,後續再呼叫resolve或reject方法,不能改變它的狀態和值。

3.Promise之then例項方法

new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('a');
    }, 1000);
}).then(function (value) {               
    console.log("第一個" + value);
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(value + 'b');
        }, 1000);
    })
}).then(function (value) {              
    console.log("第二個" + value);
}).then(function (value) {
    console.log("第三個" + value);
    console.log(a);
}).then(function (value) {              
    console.log("第四個" + value);
}, (err) => {
    console.log("第四個error", err);
})

// 第一個a
// 第二個ab
// 第三個undefined
// 第四個error ReferenceError: a is not defined
複製程式碼

then方法是Promise的例項方法,呼叫then後的返回值依然是一個promise物件,注意它是全新的promise物件,一般可以看到then的鏈式呼叫,這裡需要注意區別於jQuery的鏈式呼叫。jQuery是返回撥用物件本身。當鏈式呼叫時要注意不能被它繞暈了,要抓住一個重點,我們只是在呼叫then方法而已,給它傳參只是定義函式,並沒有執行!什麼時候執行?是根據你的非同步操作後的promise狀態如何更新以及何時更新而確定。 傳給then的回撥函式中的返回值影響著最終返回出的promise物件,引數的返回值一般有三種情況。

  • 一個普通的同步值,或者沒寫返回值預設就是undefined,當然它也屬於普通同步值。則then最終返回的是狀態是resolve成功的Promise物件,如上段程式碼的第三個輸出,它的前一個then方法內部沒有返回值則預設undefined,接下來就直接走進第三個then方法,且值value就是undefined
  • 返回新的Promise物件,then方法將根據這個Promise的狀態和值建立一個新的Promise物件返回。如第二個輸出,會等待上個then方法返回的新Promise物件狀態的更新來確定,且會等待它的更新以及將最後的值傳過來,這種情況也是當有多級非同步操作所使用的方式。
  • throw一個同步異常,then方法將返回一個rejected狀態的Promise, 值是該異常。如第四個輸出!

4.Promise之catch例項方法

Promise.prototype.catch方法是then(null, rejection)的別名,用於指定發生錯誤時的回撥函式。

let p = new Promise((resolve, reject) => {
    //
});
p.then((val) => console.log('fulfilled:', val))
  .catch((err) => console.log('rejected', err));

// 等同於
p.then((val) => console.log('fulfilled:', val))
  .then(null, (err) => console.log("rejected:", err));
複製程式碼

catch方法,它首先是捕捉處理錯誤,不論是promise呼叫了reject方法還是直接丟擲錯誤,都會走到catch方法內進行處理。接下來就和then方法一樣,返回的也是一個全新的Promise物件,錯誤處理的回撥函式返回值同樣有三種情況,具體看上個then方法。

let p = new Promise((resolve, reject) => {
    reject('失敗')
});
p.then((val) => console.log('1then: success', val))
 .then((val) => console.log('2then: success', val))
 .catch((val) => console.log('3catch: error', val))
 .catch((val) => console.log('4catch: error', val))
 .then((val) => console.log('5then: success', val))

// 控制檯輸出

// 3catch: error 失敗
// 5then: success undefined
複製程式碼

Promise 物件的錯誤具有“冒泡”性質,會一直向後傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch語句捕獲。 上段程式碼首先p這個Promise物件(狀態是resolved)遇到第一個then會忽略掉它定義的成功回撥,注意此時呼叫完第一個then方法後的返回值是全新的Promise物件!且狀態同樣是resolved,為何會這樣?因為它把p的狀態進行了一層包裝也就作為了自己的狀態,且值也和它一樣!所以說Promise的狀態具有傳遞性。

因為這個錯誤目前並沒有被捕獲處理,所以繼續向後傳遞。同樣遇到第二個then時我們可以當做跳過它,但發生的細節和第一個then同理,直到3catch將這個錯誤捕獲,所以輸出3catch: error 失敗。上面也提到catch也就是then的一個別名而已,本質其實差不多。故此時catch呼叫後的返回值再次是一個全新的promise物件,那狀態呢?因為這邊給catch傳遞的引數並沒有定義返回值,所以預設就是一個同步值undefined,則catch返回的promise物件的狀態就是resolved。那麼它呼叫最後一個then輸出5then: success undefined,也就不難理解了。

5.Promise之resolve、reject靜態方法

let p1 = Promise.resolve('p1');
p1.then(val => console.log('success', val), val => console.log('error', val))

let p2 = Promise.reject('p2');
p2.then(val => console.log('success', val), val => console.log('error', val))
複製程式碼

當傳入引數是一般同步值時則返回一個狀態為resolve或reject的Promise物件,值也就是傳入的引數,相應的會呼叫成功或失敗的回撥。

let p1 = Promise.resolve(1);
let p2 = Promise.resolve(p1);
let p3 = new Promise(function (resolve, reject) {
    resolve(p1);
});

console.log(p1 === p2)
console.log(p1 === p3)

p1.then((value) => { console.log('p1=' + value)})
p2.then((value) => { console.log('p2=' + value)})
p3.then((value) => { console.log('p3=' + value)})

// 控制檯輸出:
// true
// false
// p1=1
// p2=1
// p3=1
複製程式碼

當傳入一個Promise物件時,則resolve就直接返回該Promise物件,故p1 === p2true,p3則為全新的Promise物件,但是它狀態立刻變為resolve且值為p1,它會獲取p1的狀態和值作為自己的值。故p3=1

6.Promise之all、race靜態方法

function timeout(who) {
    return new Promise(function (resolve, reject) {
        let wait = Math.ceil(Math.random() * 3) * 1000;
        setTimeout(function () {
            if (Math.random() > 0.5) {
                resolve(who + ' inner success');
            }
            else {
                reject(who + ' inner error');
            }
        }, wait);
        console.log(who, 'wait:', wait);
    });
}

let p1 = timeout('p1');
let p2 = timeout('p2');

p1.then((success) => { console.log(success) }).catch((error) => { console.log(error) })
p2.then((success) => { console.log(success) }).catch((error) => { console.log(error) })

// race只要有一個狀態改變那就立即觸發且決定整體狀態失敗還是成功.
// all只要有一個失敗那就立即觸發整體失敗了,兩個都成功整體才成功.
Promise.all([p1, p2])
    .then((...args) => {
        console.log('all success', args)
    })
    .catch((...args) => {
        console.log('someone error', args)
    })

// 控制檯輸出(情況1)
// p1 wait: 3000
// p2 wait: 1000p2 inner error
// someone error [ 'p2 inner error' ]
// p1 inner success

// 控制檯輸出(情況2)
// p1 wait: 2000
// p2 wait: 2000
// p1 inner success
// p2 inner success
// all success [ [ 'p1 inner success', 'p2 inner success' ] ]

複製程式碼

all、race方法接受陣列作為引數,且陣列每個成員都為Promise物件。如果不是的話就呼叫Promise.resolve方法,將其轉為 Promise 例項,再進一步處理。使用表示要包裝的多個promise非同步操作來確定。具體可以看程式碼理解,要多動手自己試驗!

如有錯誤或疑問歡迎指正留言:

參考文獻:

阮一峰ECMAScript 6 入門 Promise

八段程式碼徹底掌握 Promise

相關文章