ES6 Promise 對象(學習筆記)

1. Promise 的兩個特點

(1)對象的狀態不受外界影響。Promise 對象代表一個異步操作,有三種狀態:pending(進行中)、fulfilled(已成功)、rejected(已失敗)。只有異步操作的結果可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這個也是 Promise 這個名字的由來,代表只要“承諾”了便無法改變。

(2)一旦狀態確定,就不會再變,任何時候都可以得到這個結果。Promise 對象的狀態只有兩種情況,從 pending 到 fulfilled 和從 pending 到 rejected。只要這兩種情況發生了,狀態就凝固了,會一直保持這個結果,這時就成爲 resolved(已定型)。如果已定型,你再對 Promise 對象添加回調函數,也會立即得到這個結果。這與事件(Event)完全不同,事件的特點是如果你錯過了它再去監聽,是沒有結果的。

有了 Promise 對象,就可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。此外,Promise 對象提供統一的接口,使得異步操作更加容易。

但是,Promise 對象也有一定的缺點:首先,無法取消 promise,一旦建立它就會立即執行,無法中途取消。其次,如果不設置回調函數,promise 內部拋出的錯誤不會反應到外部。第三,當處於 pending 狀態時,無法得知目前進展到哪個階段(是剛剛開始還是即將完成)。

若某些事件反覆地發生,一般來說,使用 stream 模式是比部署 Promise 更好的選擇。

2. 基本語法

ES6 規定,Promise 對象是一個構造函數,用來生成 Promise 實例

const promise = new Promise(function(resolve,reject){//函數裏的兩個參數由 JavaScript 提供,不用自己部署
     //some code
     if(/*異步操作成功*/){
         resolve(value);
     }else{
         reject(error);
     }
});

Promise 實例生成以後,可以用 then 方法分別指定 resolved 狀態和 rejected 狀態的回調函數

then 中的兩個參數,第二個可選,都接受 promise 對象傳出的值作爲參數
promise.then(function(value){
   //success
},function(error){
   //failure
});

下面是一個簡單的 Promise 對象的例子

function timeout(ms){
   return new Promise((resolve,reject) => {
      setTimeout(resolve,ms,'done');
   });
}

timeoue(100).then((value) =>{
   console.log(value);
});

Promise 一旦建立就會立即執行,then 方法指定的回調函數,在當前腳本所有同步任務執行完後纔會執行。

下面是加載圖片的例子

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;
   });
}

resolve 函數和 reject 函數的參數會傳遞給回調函數,它們的參數也可以是一個 Promise 對象,即一個異步操作的結果是返回另一個異步操作。

const p1 = new Promise(function(resolve,reject){
     setTimeout(() => reject(new Error('fail')),3000);
});

const p2 = new Promise(function(resolve,reject){
    setTimeout(() => resolve(p1),1000);
});

p2
  .then(resolve => console.log(resolve))
  .catch(error => console.log(error))

//Error:fail

/*由於 p2 返回的是另一個 Promise 的狀態,導致 p2 自己的狀態無效了,由 p1 的狀態控制 p2 的狀態,
所以後面的 then 語句和 catch 語句都變成針對 p1 了。執行時,p1 3 秒之後變爲 rejected ,p2 的狀態
在 1 秒後改變,返回 p1 。又過了 2 秒後, p1 變爲 rejected,導致觸發 catch 方法指定的回調函數。*/

若 Promise 對象中,resolve 方法(reject 同)後面還有語句,則後面的語句會先執行,這是因爲 resolve 總會晚於本輪循環的同步任務。所以我們最好在 resolve 前面加上 return 語句,這樣就不會有意外

//例1
const p = new Promise(function(resolve,reject){
    resolve(1);
    console.log(2);
}).then(r => {
    console.log(r);
});

//2
//1

//例2
const p = new Promise(function(resolve,reject){
    return resolve(1);  
    console.log(2);     //不會執行
});

3. Promise.prototype.then():then 方法的第一個參數是 resolved 狀態的回調函數,第二個參數(可選)是 rejected 狀態的回調參數。一般來說,不要在 then() 方法裏面定義 Rejected 狀態的回調函數(then 的第二個參數),總是使用 catch 方法比較好。

4. Promise.prototype.catch():這個方法是 .then(null,rejection) 或 .then(undefined,rejection) 的別名,用於指定發生錯誤時的回調函數。Promise 對象的錯誤具有 “冒泡” 性質,會一直向後傳遞,直到被捕獲爲止。

getJson('/posts.json').then(function(posts){
    //...
}).catch(function(error){
    //處理 getJson 和前一個回調函數運行時發生的錯誤
    console.log("發生錯誤! ",error);
});

reject()的作用,等同於拋出錯誤。

const promise = new Promise(function(resolve,reject){
     throw new Error('test');
});

promise.catch(function(error){
     console.log(error);
});

//Error:test

//等同於
const promise = new Promise(function(resolve,reject){
     try{
          throw new Error('test');
     }catch(e){
          reject(e);
     }
});

promise.catch(function(error){
     console.log(error);
});

//等同於
const promise = new Promise(function(resolve,reject){
     reject(new Error('test'));
});

promise.catch(function(error){
     console.log(error);
});

跟傳統 try/catch 代碼塊不同的是,若沒有使用 catch()指定錯誤處理的回調函數,Promise 對象發生的錯誤不會傳遞到外層代碼,即不會做出任何反應

const someAsyncThing = function(){
    return new Promise(function(resolve,reject){
        //下面一行信息報錯,因爲 x 沒有聲明
        resolve(x + 2);
    });
};

someAsyncThing().then(() => {console.log('everything is great')});

setTimeout(() => {console.log(123)},2000);

// Uncaught (in promise) ReferenceError: x is not defined
// 123

以上代碼說明,Promise 內部的錯誤不會影響到 Promise 外部的代碼,通俗的說法就是“ Promise 會喫掉錯誤”。這個腳本放在服務器執行,退出碼就是 0 (即表示執行成功)。

不過,Node.js 有一個專門監聽未捕獲的 reject 錯誤的事件unhandleRejection,可以在監聽函數裏面拋出錯誤。第一個參數是錯誤對象,第二個參數是報錯的 Promise 實例,它可以用來了解發生錯誤的環境信息。

process.on('unhandleRejection',function(error,p){
    throw error;
});

注意,Node 有計劃在未來廢除 unhandleRejection 事件。如果 Promise 內部有未捕獲的錯誤,會直接終止進程,並且進程的退出碼部位 0。

5. Promise.prototype.finally():用於指定不管 Promise 對象狀態最後如何,都會執行的操作。finally() 不接受任何參數。

以下一個例子是服務器使用 Promise 處理請求,最後使用 finally 方法關掉服務器。

server.listen(port)
  .then(function(){
      //...
  })
  .finally(server.stop);

6. Promise.all():用於將多個 Promise 實例,包裝成一個 Promise 實例。其參數可以不是數組,但必須是 Iterator 接口,且返回的每個成員都是 Promise 實例。

const p = Promise.all([p1,p2,p3]);

其中 p1,p2,p3 都是 Promise 實例,若不是實例,就會先調用 Promise.resolve(),將參數轉成 Promise 實例。

p 的狀態由 p1,p2,p3 決定

(1)只有當 p1,p2,p3 的狀態都爲 fulfilled , p 的狀態纔會爲 fulfilled,此時 p1,p2,p3 的返回值組成一個數組,傳遞給 p 的回調函數

(2)只要 p1,p2,p3 中有一個狀態爲 rejected,p 的狀態就會爲 rejected,此時第一個被 rejected 的實例的返回值,會傳遞給 p 的回調函數。 

注意,若作爲參數的 Promise 實例,自己定義了 catch 方法,那麼它一旦被 rejected ,就不會出發 Promise.all() 的 catch 方法。

const p1 = new Promise(function(resolve,reject){
    resolve("hello");
})
  .then(result => result)
  .catch(e => e);

const p2 = new Promise(function(resolve,reject){
    throw new Error("報錯了");
})
  .then(result => result)
  .catch(e => e);

Promis.all([p1,p2])
  .then(result => console.log(result))
  .catch(e => console.log(e));

//["hello",Error:報錯了]

//p1 的狀態爲 resolved,p2 實例執行完 catch 後,狀態也會變成 resolved,故 Promise.all() 的狀態也是 resolved ,因此會調用 then()指定的回調函數。

若 p2 沒有自己的 catch 方法,就會調用 Promise.all() 的 catch 方法。

const p1 = new Promise(function(resolve,reject){
    resolve("hello");
})
  .then(result => result)
  .catch(e => e);

const p2 = new Promise(function(resolve,reject){
    throw new Error("報錯了");
})
  .then(result => result);

Promis.all([p1,p2])
  .then(result => console.log(result))
  .catch(e => console.log(e));

//Error:報錯了

7. Promise.race():同樣是將多個 Promise 實例包裝成一個 Promise 實例。

Promise.race() 和 Promise.all() 一樣,如果參數不是 Promise 實例,會先調用 Promise.resolve()將參數轉爲 Promise 實例再進一步處理。

下面是一個例子,若在指定時間內沒有獲得結果,就將 Promise 的狀態變爲 rejected,否則變爲 resolved。

const p = Promise.race([
     fetch('/resource-that-may-take-a-while'),
     new Promise(function(resolve,reject){
        setTimeout(() => reject(new Error('request timeout')),3000);
     })
 ]);

p
  .then(console.log)
  .catch(console.error);
 
//若在 3 秒之內,fetch 方法無法返回結果,變量 p 的狀態就會變爲 rejected,從而觸發 catch 指定的方法回調函數

8. Promise.allSettled():接受一組 Promise 實例作爲參數,包裝成一個新的 Promise 實例。只有等到所有這些實例都返回結果,無論是 fulfilled 還是 rejected,包裝實例纔會結束。

const promises = [
   fetch('/api-1'),
   fetch('/api-2'),
   fetch('/api-3'),
];

await Promise.allSettled(promises);
removeLoadingIndicator();

下面是返回值用法的例子。

const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);

//過濾出成功的請求
const succesefulPromise = results.filter(p => p.status === "fulfilled");

//過濾出失敗的請求,請輸出原因
const error = results
    .filter(p => p.status === "rejected")
    .map(p => p.reason);

//每個對象都有 status 屬性,當 Promise 實例的狀態爲 fulfilled 時,status 有 value 屬性,Promise 狀態爲 rejected 時,status 有 reason 屬性。

有時候,我們不關心異步操作的結果,只關心異步操作有沒有結束。這時,Promise.allSettled() 就很有用,Promise.all() 無法做到這一點。

const urls = [/*...*/];
const requests = urls.map(x => fetch(x));

try{
   await Promise.all(requests);
   console.log("所有請求都成功");
}catch{
   console.log("至少一個請求失敗,其他請求可能還沒結束");
}

9. Promise.any():接受一組 Promise 實例作爲參數,包裝成一個新的 Promise 實例。只要參數實例有一個是 fulfilled 狀態,那麼包裝實例就會變成 fulfilled 狀態,參數實例全部都爲 rejected 狀態時,包裝實例纔會爲 rejected 狀態。

Promise.any() 與 Promise.race() 很相似,只有一點不同,就是不會因爲某個 Promise 狀態爲 rejected 而結束。

Promise.any() 拋出的錯誤不是一個一般的錯誤,而是一個 AggregateError 實例,它相當於一個數組,每個成員對應一個被 rejected 的操作所拋出的錯誤。

new AggregateError() extends Array -> AggregateError

const err = new AggregateError();
err.push(new Error('first error'));
err.push(new Error('second error'));
throw err;

下面是一個例子

var resolved = Promise.resolve(42);
var rejected = Promise.reject(-1);
var rejected2 = Promise.reject(Infinity);

Promise.any([resolved,rejected,rejected2]).then(function(result){
    console.log(result);  //42
});

Promise.any([rejected,rejected2]).catch(function(result){
    console.log(result);  //[-1 Infinity]
});

10. Promise.resolve():將現有對象轉爲 Promise 實例。

Promise.resolve('foo');

//等同於 

new Promise(resolve => resolve('foo'));

Promise.resolve() 的參數分成 4 中情況。

(1)參數是 Promise 實例,那麼 Promise.resolve() 將不做任何修改,原封不動的返回這個實例。

(2)參數是一個 thenable 對象(即具有 then()的對象),將其轉爲 Promise 實例之後,立即執行 thenable 對象的 then 方法

let thenable = {
   then: function(resolve,reject){
       resolve(42);  
   }
};

let p1 = Promise.resolve(thenable);
p1.then(function(value){
   console.log(value);  //42
});

(3)參數不是具有 then 方法的對象,或者根本不是對象,則 Promise.resolve() 返回一個新的對象,狀態爲 resolved。

const  p = Promise.resolve('hello');

p.then(function(s){
   console.log(s);  //hello
});

(4)不帶任何參數,直接返回一個 resolved 狀態的 Promise 對象。若希望得到一個 Promise 對象,最簡單的方法就是用 Promise.resolve()

需要注意的是,立即 resolve()的 Promise 對象,是在本輪事件循環(event loop)的結束時執行,而不是在下一輪事件循環的開始時。

setTimeout(function(){
   console.log("three");
},0);

Promise.resolve().then(function(){
  console.log('two');
});

console.log('one');

//one
//two
//three

11. Promise.reject():也會返回一個 Promise 實例,狀態爲 rejected

注意,Promise.reject() 的參數,會原封不動的作爲 reject 的理由,變成後續方法的參數,這一點與 Promise.resolve() 不同。

const thenable = {
   then:function(resolve,reject){
       reject('出錯了');
   }
};

Promise.reject(thenable)
  .catch(e =>{
     console.log(e === thenable);  //true
  });

以上代碼中 Promise.reject() 的參數是一個 thenable 對象,執行以後,後面 catch 的參數不是 reject()拋出的 “出錯了”,而是 thenable 對象。

12. Promise.try():讓同步函數同步執行,異步函數異步執行,並且有統一的 API。

第一種寫法:

const f = () => console.log('now');

(async () => f())();  //立即執行函數

console.log('next');

//now
//next

以上代碼,若 f 是同步的,就會得到同步的結果,若 f 是異步的,就可以用 then 指定下一步,如

(async () => f())()
  .then(...)

需要注意的是,async() 會喫掉 f() 拋出的錯誤,如果想要捕獲錯誤,要使用 promise.catch()

(async () => f())()
  .then(...)
  .catch(...)

第二種寫法:使用 new Promise()

const f = () => console.log('now');

(
   () => new Promise(resolve,reject){
       resolve => resolve(f());
   }
)();

cosole.log('next');
//now
//next

鑑於這是一個很常見的需求,所以有一個提案,用 Promise.try() 代替以上的寫法

const f = () => console.log('now');

Promise.try(f);

console.log('next');

//now
//next

 

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