JavaScript異步編程系列(二)——Promise

建立Promise

Promise是一個JavaScript標準內置對象。用來存儲一個異步任務的執行結果,以備未來使用。javascript

建立一個Promise對象:java

let promise = new Promise(function(resolve, reject) {
  // executor
});
複製代碼

構造函數Promise接收一個函數(稱爲執行器executor)做爲參數,並向函數傳遞兩個函數做爲參數:resolve和reject。git

建立的Promise實例對象(如下用promise代替)具備如下內部屬性:github

  • state:表示promise的狀態,初始值是"pending",調用resolve後變爲"fulfilled",調用reject後變爲"rejected"。
  • result:表示promise的結果,初始值是undefined,調用resolve後變爲value,調用reject後變爲error。

當執行new Promise時,executor會當即執行。能夠在裏面書寫須要處理的異步任務,同步任務也支持。 任務處理完獲得的結果,須要調用如下回調之一:json

  • resolve(value):能夠將任務執行成功的結果做爲參數傳遞給resolve並調用,此時promise的state變爲"fulfilled"。
  • reject(error):能夠將任務執行出現的Error對象做爲參數傳遞給reject並調用,此時promise的state變爲"rejected"。

resolve/reject只須要一個參數(或不包含任何參數),多餘會被忽略。api

一個已經「settled」的promise(狀態已經變爲"fulfilled"或"rejected")將不能再次調用resolve或reject,即resolve或reject只能調用一次。剩下的resolve和reject的調用都會被忽略。數組

示例:promise

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("done"), 1000);
});

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});
複製代碼

消費promise

經過.then、.catch和.finally來消費promise。瀏覽器

promise的state和result屬性都是內部的,沒法直接訪問它們。 但咱們可使用 .then/.catch/.finally 來訪問。markdown

1. then

.then(f, f) 接收兩個函數參數: - 第一個函數在promise resolved後執行並接收結果做爲參數; - 第二個函數(非必填)在promise rejected後執行並接收error做爲參數;

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("done!"), 1000);
});

promise.then(
  result => alert(result), // 1 秒後顯示 "done!"
  error => alert(error) // 不運行
);
複製代碼
2. catch

.catch(f) 接收一個函數做爲參數,該函數接收reject的error做爲參數,是.then(null, f)的簡寫形式。

let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

promise.catch(alert); // 1 秒後顯示 "Error: Whoops!"
複製代碼
3. finally

.finally(f) 接收一個無參數的函數,不論promise被resolve或reject都會執行。

new Promise((resolve, reject) => {
  // ...
}).finally(() => stop loading indicator)
  .then(result => show result, err => show error);
複製代碼

finally會繼續將promise的結果傳遞下去。

4. 執行順序

.then/.catch/.finally異步執行:會等到promise的狀態由pending變爲settled時,當即執行。

更確切地說,當 .then/catch 處理程序應該執行時,它會首先進入內部隊列。JavaScript 引擎從隊列中提取處理程序,並在當前代碼完成時執行 setTimeout(..., 0)。

換句話說,.then(handler) 會被觸發,會執行相似於 setTimeout(handler, 0) 的動做。

在下述示例中,promise 被當即 resolved,所以 .then(alert) 被當即觸發:alert 會進入隊列,在代碼完成以後當即執行。

// 一個被當即 resolved 的 promise
let promise = new Promise(resolve => resolve("done!"));

promise.then(alert); // done!(在當前代碼完成以後)

alert("code finished"); // 這個 alert 會最早顯示
複製代碼

所以在 .then 以後的代碼老是在處理程序以前被執行(即便是在預先 resolved 的 promise 的狀況下)。

Promise鏈

promise.then(f)的處理程序f(handler)調用後返回一個promise,handle自己返回的值會做爲這個promise的result;result能夠傳遞給下一個.then處理程序鏈進行傳遞。 對同一個promise分開.then時,每一次的結果都同樣,由於.then只是單純使用了promise提供的result,並不改變原來的promise自己。

new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
  alert(result); // 1
  return result * 2;
}).then(function(result) { // (***)
  alert(result); // 2
  return result * 2;
}).then(function(result) {
  alert(result); // 4
  return result * 2;
});
複製代碼

.then(handler) 中所使用的處理程序(handler)能夠建立並返回一個 promise。 此時其餘的處理程序(handler)將等待它 settled 後再得到其result

new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000);
}).then(function(result) {
  alert(result); // 1
  return new Promise((resolve, reject) => { // (*)
    setTimeout(() => resolve(result * 2), 1000);
  });
}).then(function(result) { // (**)
  alert(result); // 2
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 2), 1000);
  });
}).then(function(result) {
  alert(result); // 4
});
複製代碼
thenable對象

handler也能夠返回一個「thenable」 對象 —— 一個具備方法 .then 的任意對象。它會被當作一個 promise 來對待。 第三方庫能夠實現本身的promise兼容對象。它們能夠具備擴展的方法集,但也與原生的 promise 兼容,由於它們實現了 .then 方法。

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve); // function() { native code }
    // 1 秒後使用 this.num*2 進行 resolve
    setTimeout(() => resolve(this.num * 2), 1000); // (**)
  }
}

new Promise(resolve => resolve(1))
  .then(result => {
    return new Thenable(result); // (*)
  })
  .then(alert); // 1000ms 後顯示 2
複製代碼

handler返回的對象若是具備then方法,會被當即調用並接收resolve和reject做爲參數。直到resolve或reject執行後,再將result傳遞給下一個.then,以此類推,沿着鏈向下傳遞。

這個特性能夠用來將自定義的對象與 promise 鏈集成在一塊兒,而沒必要繼承自 Promise。 推薦異步行爲始終返回一個promise以便後續對鏈進行擴展。

若是 .then(或 catch)處理程序(handler)返回一個 promise,那麼鏈的其他部分將會等待,直到它狀態變爲 settled。當它被 settled 後,其 result(或 error)將被進一步傳遞下去。

比較promise.then(f1, f2);promise.then(f1).catch(f2);

  • 前者:沒有鏈,因此f1出現錯誤後不會被處理
  • 後者:error做爲result會沿着鏈傳遞,因此f1中出現error後會被.catch處理

Promise的方法

Promise類有5種靜態方法:

  • Promise.resolve(value) – 根據給定值返回 resolved promise。
  • Promise.reject(error) – 根據給定錯誤返回 rejected promise。
  • Promise.all(promises) – 等待全部的 promise 爲 resolve 時返回存放它們結果的數組。若是任意給定的 promise 爲 reject,那麼它就會變成 Promise.all 的錯誤結果,全部的其餘結果都會被忽略。
  • Promise.allSettled(promises) (新方法) – 等待全部 promise resolve 或者 reject,並以對象形式返回它們結果數組:
    • state:‘fulfilled’ 或 ‘rejected’
    • value(若是fulfilled)或 reason(若是 rejected)]
  • Promise.race(promises) – 等待第一個 promise 被解決,其結果/錯誤即爲結果。

這五個方法中,Promise.all 在實戰中使用的最多。

1. Promise.resolve

let promise = Promise.resolve(value) —— 根據給定的 value 值返回 resolved promise。 等價於:let promise = new Promise(resolve => resolve(value));

2. Promise.reject(實際工做中少用)

let promise = Promise.reject(error) —— 建立一個帶有 error 的 rejected promise。 等價於:let promise = new Promise((resolve, reject) => reject(error));

3. Promise.all

let promise = Promise.all([...promises...]); —— 並行執行多個promise,返回一個新的promise,其結果爲包含全部promise的結果的有序數組。參數爲一個promise數組(嚴格能夠是任何可迭代對象,一般是數組)。

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(alert); // 1,2,3 當 promise 就緒:每個 promise 即成爲數組中的一員

// 經常使用來發送並行請求
let names = ['iliakan', 'remy', 'jeresig'];
let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));
Promise.all(requests)
  .then(responses => {
    // 全部響應都就緒時,咱們能夠顯示 HTTP 狀態碼
    for(let response of responses) {
      alert(`${response.url}: ${response.status}`); // 每一個 url 都顯示 200
    }
    return responses;
  })
  // 映射 response 數組到 response.json() 中以讀取它們的內容
  .then(responses => Promise.all(responses.map(r => r.json())))
  // 全部 JSON 結果都被解析:「users」 是它們的數組
  .then(users => users.forEach(user => alert(user.name)));
複製代碼

若是任意一個 promise爲reject,Promise.all返回的 promise 就會當即 reject 這個錯誤。並忽略全部列表中其餘的 promise。它們的結果也被忽略。

Promise.all([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: Whoops!
複製代碼

Promise.all(...) 接受可迭代的 promise 集合(大部分狀況下是數組)。可是若是這些對象中的任意一個不是 promise,它將會被直接包裝進 Promise.resolve。

Promise.all([
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000)
  }),
  2, // 視爲 Promise.resolve(2)
  3  // 視爲 Promise.resolve(3)
]).then(alert); // 1, 2, 3
複製代碼
4. Promise.allSettled(最近添加的新特性,老瀏覽器須要polyfills)

用法同Promise.all,只不過Promise.allSettled會等待全部的 promise 都被處理:即便其中一個 reject,它仍然會等待其餘的 promise。處理完成後的數組由如下對象組成: {status:"fulfilled", value:result} 對於成功的響應, {status:"rejected", reason:error} 對於錯誤的響應。

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://no-such-url'
];

Promise.allSettled(urls.map(url => fetch(url)))
  .then(results => {
    /* results: [ {status: 'fulfilled', value: ...response...}, {status: 'fulfilled', value: ...response...}, {status: 'rejected', reason: ...error object...} ]*/
    results.forEach((result, num) => {
      if (result.status == "fulfilled") {
        alert(`${urls[num]}: ${result.value.status}`);
      }
      if (result.status == "rejected") {
        alert(`${urls[num]}: ${result.reason}`);
      }
    });
  });
複製代碼

若是瀏覽器不支持 Promise.allSettled,使用 polyfill 很容易讓其支持:

if(!Promise.allSettled) {
  Promise.allSettled = function(promises) {
    // p => Promise.resolve(p) 將該值轉換爲 promise(以防傳遞了非 promise)
    return Promise.all(promises.map(p => Promise.resolve(p).then(
      v => ({ state: 'fulfilled', value: v }), 
      r => ({ state: 'rejected', reason: r })
    )));
  };
}
複製代碼
5. Promise.race

let promise = Promise.race(iterable); —— 與 Promise.all 相似,它接受一個可迭代的 promise 集合,可是隻要有一個promise被settled了就會中止等待,將這個promise的結果/錯誤做爲它的結果,其餘的promise的結果/錯誤都會被忽略。

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1
複製代碼

錯誤處理

  1. 在Promise的執行器executor和處理器handler程序(.then/.catch/.finally中的函數參數)中,若是發生錯誤,或調用reject(),都會將promise變爲rejected,將錯誤傳遞給最近的錯誤處理程序.catch。 就好像代碼周圍有一個不可見的try..catch
  2. 在鏈式調用中,最末端加上.catch來處理上面的全部錯誤,只要有一個錯誤,控制權就會直接傳遞到最近的.catch
  3. catch處理完錯誤後,catch後面能夠.then繼續處理
// 1. 代碼執行錯誤
new Promise((resolve, reject) => {
  throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!

// 1. 主動調用reject
new Promise((resolve, reject) => {
  reject(new Error("Whoops!"));
}).catch(alert); // Error: Whoops!

// 2. 處理器中的錯誤
new Promise((resolve, reject) => {
  resolve("ok");
}).then((result) => {
  throw new Error("Whoops!"); // rejects 這個 promise
}).then((result) => {
  // 這個then不執行
}).catch(alert); // Error: Whoops!

// 3. 執行:catch -> then
new Promise((resolve, reject) => {
  throw new Error("Whoops!");
}).catch(function(error) {
  alert("The error is handled, continue normally");
}).then(() => alert("Next successful handler runs"));
複製代碼
未捕獲的錯誤

若是promise變爲rejected,可是卻沒有catch處理這個錯誤,錯誤就被卡住(stuck),腳本就會死掉。

JavaScript引擎會跟蹤此類rejections,生成一個全局錯誤,能夠監聽unhandledrejection事件捕獲。

window.addEventListener('unhandledrejection', function(event) {
  // the event object has two special properties:
  alert(event.promise); // [object Promise] - 產生錯誤的 promise
  alert(event.reason); // Error: Whoops! - 未處理的錯誤對象
});

new Promise(function() {
  throw new Error("Whoops!");
}); // 沒有 catch 處理錯誤
複製代碼

建議將.catch放在想要處理錯誤的位置,自定義錯誤類來幫助分析錯誤,還能夠從新拋出錯誤。 若是發生錯誤後沒法恢復腳本,那不用catch處理錯誤也行,可是應該使用unhandledrejection事件來跟蹤錯誤。 使用finally處理必需要發生的任務,好比關閉loading。

有一個瀏覽器技巧是從 finally 返回零延時(zero-timeout)的 promise。這是由於一些瀏覽器(好比 Chrome)須要「一點時間」外的 promise 處理程序來繪製文檔的更改。所以它確保在進入鏈下一步以前,指示在視覺上是中止的。

沒法捕獲的錯誤 todo

在setTimeout中拋出的錯誤沒法被catch捕獲:

const promise = new Promise(function(resolve, reject) {
  setTimeout(function () { throw new Error('test') }, 0)
});
promise.catch(function(error) { console.log(error) });
複製代碼

除非顯式調用reject:

const promise = new Promise(function(resolve, reject) {
  setTimeout(function () { 
    reject(new Error('test'));
 }, 0)
});
複製代碼

緣由:JS事件循環列表有宏任務與微任務之分:setTimeOut是宏任務, promise是微任務,他們有各自的執行順序;所以這段代碼的執行順序是:

  1. 代碼執行棧進入promise觸發setTimeOut,此時setTimeOut回調函數加入宏任務隊列
  2. 代碼執行promise的catch方法(微任務隊列)此時setTimeOut回調尚未執行
  3. 執行棧檢查發現當前微任務隊列執行完畢,開始執行宏任務隊列
  4. 執行throw new Error('test')此時這個異常實際上是在promise外部拋出的 但若是在setTimeOut中主動觸發了promise的reject方法,所以promise的catch將會在setTimeOut回調執行後的屬於他的微任務隊列中找到它而後執行,因此能夠捕獲錯誤

Promise與微任務

Promise 的處理程序(handlers).then、.catch 和 .finally 都是異步的。 異步任務須要適當的管理。爲此,JavaScript 標準規定了一個內部隊列 PromiseJobs —— 「微任務隊列」(Microtasks queue)(v8 術語)。 這個隊列先進先出,只有引擎中沒有其餘任務運行時纔會啓動任務隊列的執行。 當一個 promise 準備就緒時,它的 .then/catch/finally 處理程序就被放入隊列中。等到當前代碼執行完而且以前排好隊的處理程序都完成時,JavaScript引擎會從隊列中獲取這些任務並執行。 即使一個 promise 當即被 resolve,.then、.catch 和 .finally 以後的代碼也會先執行。 若是要確保一段代碼在 .then/catch/finally 以後被執行,最好將它添加到 .then 的鏈式調用中。

let promise = Promise.resolve();

promise.then(() => alert("promise done"));

alert("code finished"); // 該警告框會首先彈出
複製代碼
未處理的 rejection

指在 microtask 隊列結束時未處理的 promise 錯誤。 microtask隊列完成時,引擎會檢查promise,若是其中任何一個出現rejected狀態,就會觸發unhandledrejection事件。 但若是在setTimeout裏進行catch,unhandledrejection會先觸發,而後catch才執行,因此catch沒有發揮做用。

let promise = Promise.reject(new Error("Promise Failed!"));
setTimeout(() => promise.catch(err => alert('caught')));
window.addEventListener('unhandledrejection', event => alert(event.reason));
// Promise Failed! -> caught
複製代碼

將回調函數Promise化

簡單的示例

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;
  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));
  document.head.append(script);
}

// promise改寫:
let loadScriptPromise = function(src) {
  return new Promise((resolve, reject) => {
    loadScript(src, (err, script) => {
      if (err) reject(err)
      else resolve(script);
    });
  })
}
// 用法:
// loadScriptPromise('path/script.js').then(...)
複製代碼

通用的promisify函數:

function promisify(f) {
  return function (...args) { // 返回一個包裝函數
    return new Promise((resolve, reject) => {
      function callback(err, result) { // 給 f 用的自定義回調
        if (err) {
          return reject(err);
        } else {
          resolve(result);
        }
      }

      args.push(callback); // 在參數的最後附上咱們自定義的回調函數

      f.call(this, ...args); // 調用原來的函數
    });
  };
};

// 用法:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
複製代碼

以上回調函數只能接收兩個參數,接收多個參數的示例:

// 設定爲 promisify(f, true) 來獲取結果數組
function promisify(f, manyArgs = false) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      function callback(err, ...results) { // 給 f 用的自定義回調
        if (err) {
          return reject(err);
        } else {
          // 若是 manyArgs 被指定值,則 resolve 全部回調結果
          resolve(manyArgs ? results : results[0]);
        }
      }

      args.push(callback);

      f.call(this, ...args);
    });
  };
};

// 用法:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...)
複製代碼
相關文章
相關標籤/搜索