ES6 的異步操做之 promise 用法和 async 函數

promise 基本用法

Promise 對象是一個構造函數,用來生成 Promise 實例。Promise 構造函數接受一個函數做爲參數,該函數的兩個參數分別是 resolverejectes6

resolve 函數的做用是,在異步操做成功時調用(Promise 對象的狀態從 pending 變爲 fulfilled),並將異步操做的結果,做爲參數傳遞出去。json

reject 函數的做用是,在異步操做失敗時調用(Promise對象的狀態從 pending 變爲 rejected),並將異步操做報出的錯誤,做爲參數傳遞出去。api

const funPromise = function(options) {
  return new Promise(function(resolve, reject) {
    if (/* 異步操做成功 */){
      resolve(result);
    } else {
      reject(error);
    }
  });
}

resolve 函數的參數除了正常的值之外,還多是另外一個 Promise 實例,此時,初始 promise 的最終狀態根據傳入的新的 Promise 實例決定。數組

reject 方法的做用,至關於拋出錯誤。等同於 throw new Error('error')promise

Promise.prototype.then()

Promise 實例具備 then 方法,它的做用是爲 Promise 實例添加狀態改變時的回調函數,即 Promise 實例生成之後,用 then 方法分別指定 fulfilled 狀態和 rejected 狀態的回調函數。併發

funPromise().then(function(result) {
  // fulfilled
}, function(error) {
  // rejected
})

then 方法能夠接受兩個回調函數做爲參數。第一個回調函數是 Promise 對象的狀態變爲 fulfilled 時調用,第二個回調函數是 Promise 對象的狀態變爲 rejected 時調用。其中,第二個函數是可選的,不必定要提供。這兩個函數都接受 Promise 對象傳出的值做爲參數。app

then 方法返回的是一個新的 Promise 實例(注意,不是原來那個 Promise 實例)。所以能夠採用鏈式寫法,即 then 方法後面再調用另外一個 then 方法來處理上一個 then 方法中 return 的結果。異步

funPromise().then(function(result) {
  return result.data;
}).then(function(data) {
  // fulfilled
});

上面的代碼使用 then 方法,依次指定了兩個回調函數。第一個回調函數完成之後,會將返回結果做爲參數,傳入第二個回調函數。而且,第一個 then 返回的結果也能夠是另外一個異步操做的 Promise 對象,這時後一個 then 函數,就會等待該 Promise 對象的狀態發生變化,纔會被調用。async

funPromise().then(
  (result) => { return funPromise(result); }
).then(
  (data) => { /* fulfilled */ },
  (error) => { /* rejected */ }
);

上面代碼中,第一個 then 方法指定的回調函數,返回的是另外一個 Promise 對象。這時,第二個 then 方法指定的回調函數,就會等待這個新的 Promise 對象狀態發生變化。若是變爲 fulfilled,就調用第一個回調函數,若是狀態變爲 rejected,就調用第二個回調函數。函數

Promise.prototype.catch()

Promise 實例具備 catch 方法,它的做用是爲 Promise 實例添加狀態改變爲 rejected 狀態的回調函數,也就是 then 方法的第二個函數的替代寫法。

funPromise().then(function(result) {
  // fulfilled
}).catch(function(error) {
  // 處理 funPromise 和以前 then 回調函數運行時發生的錯誤
});

Promise 對象的錯誤具備「冒泡」性質,會一直向後傳遞,直到被捕獲爲止。也就是說,不管前面有多少個 then 函數,其中的錯誤老是會被下一個 catch 語句捕獲。

funPromise().then(function(result) {
  return funPromise(result);
}).then(function(data) {
  // fulfilled
}).catch(function(error) {
  // 處理前面三個 Promise 產生的錯誤
});

通常來講,不要在 then 方法裏面定義 rejected 狀態的回調函數(即 then 的第二個參數),老是使用 catch 方法,由於這種寫法能夠捕獲前面 then 方法執行中的錯誤。

catch 方法返回的仍是一個 Promise 對象,而且 catch 中若是沒有拋出任何其它錯誤,那麼該 Promise 對象則是 resolved 狀態。並且後面還能夠接着調用 then 方法,可是前面的 catch 不能捕獲後面的 then 中的錯誤,因此儘可能 catch 都寫在最後。

Promise.all()

Promise.all() 方法用於將多個 Promise 實例,包裝成一個新的 Promise 實例。其接受一個數組做爲參數,數組中的值都是 Promise 實例,若是不是,就會先調用 Promise.resolve() 方法,將參數轉爲 Promise 實例,再進一步處理。

const p = Promise.all([funPromise(1), funPromise(2), funPromise(3)]);

p 的狀態由數組中的值決定,分紅兩種狀況。

  • 數組中 Primise 實例的狀態都變成 fulfilledp 的狀態纔會變成 fulfilled,此時數組中實例的返回值組成一個數組,傳遞給 p 的回調函數。
  • 只要數組的實例之中有一個被 rejectedp 的狀態就變成 rejected,此時第一個被 reject 的實例的返回值,也就是報錯信息,會傳遞給 p 的回調函數。
p.then(function (results) {
  // 所有 fulfilled,results 是個數組,裏面是每一個實例的返回結果
}).catch(function(error){
  // 其中有一個變爲 rejected
});

注意,若是做爲參數的 Promise 實例,本身定義了 catch 方法,那麼它一旦被 rejected,並不會觸發 Promise.all()catch 方法。

應用

Promise 對象實現 Ajax

const getAjax = function(url) {
  const promise = new Promise(function(resolve, reject){
    const handler = function() {
      if (this.readyState === 4 && this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.onreadystatechange = handler;
    xhr.responseType = "json";
    xhr.setRequestHeader("Accept", "application/json");
    xhr.send();
  });
  return promise;
};

getAjax("/test.json").then(function(json) {
  console.log('Contents: ' + json);
}, function(error) {
  console.error('出錯了', error);
});

async / await 基本用法

async 函數執行的時候,一旦遇到 await 就會先等到 await 後的異步操做完成,再接着執行函數體內以後的語句。

async 函數返回一個 Promise 對象,可使用 then 方法添加回調函數。async 函數內部 return 語句返回的值,會成爲 then 方法回調函數的參數。

async function f() {
  return 'hello dora';
}

f().then(v => console.log(v))   // "hello dora"

async 函數內部拋出錯誤,會致使返回的 Promise 對象變爲 rejected 狀態。拋出的錯誤對象會被 catch 方法回調函數接收到。

async function f() {
  throw new Error('出錯了');
}

f().catch( e => console.log(e))  // Error: 出錯了

await 命令

正常狀況下,await 命令後面是一個 Promise 對象,返回該對象的結果。若是不是 Promise 對象,就直接返回對應的值。

async function f() {
  return await 123;             // 等同於 return 123;
}

f().then(v => console.log(v))  // 123

await 命令後面的 Promise 對象若是變爲 rejected 狀態,則錯誤會被 catch 方法的回調函數接收到。

任何一個 await 語句後面的 Promise 對象變爲 rejected 狀態,那麼整個 async 函數就會中斷執行。

有時,咱們但願即便前一個異步操做失敗,也不要中斷後面的異步操做,有兩個解決辦法:

第一種方法是能夠將 await 放在 try...catch 結構裏面,這樣無論這個異步操做是否成功,後面的代碼都會執行。

async function f() {
  try {
    await Promise.reject('出錯了');
  } catch(e) { }
  return await Promise.resolve('hello dora');
}

f().then(v => console.log(v))   // hello dora

另外一種方法是 await 後面的 Promise 對象再跟一個 catch 方法,處理前面可能出現的錯誤。

async function f() {
  await Promise.reject('出錯了').catch(e => console.log(e));
  return await Promise.resolve('hello dora');
}

f().then(v => console.log(v))
// 出錯了
// hello dora

使用注意點

1. 錯誤處理

前面已經說過,await 命令後面的 Promise 對象,運行結果多是 rejected,因此防止出錯的方法,就是最好把 await 命令放在 try...catch 代碼塊中。若是有多個 await 命令,能夠統一放在 try...catch 結構中,若是隻有一個 await,可使用上例中的 catch 捕獲 await 後面的 promise 拋出的錯誤。

const superagent = require('superagent');
const NUM_RETRIES = 3;

async function test() {
  let i;
  for (i = 0; i < NUM_RETRIES; i++) {
    try {
      await superagent.get('/api/xxx');
      break;
    } catch(err) {}
  }
}

test();

上面代碼中,使用 try...catch 結構,實現屢次重複嘗試。若是 await 操做成功,就會使用 break 語句退出循環;若是失敗,會被 catch 語句捕捉,而後進入下一輪循環。

2. 多個 await 異步操做併發執行

多個 await 命令後面的異步操做,若是不存在繼發關係(即互不依賴),最好讓它們同時觸發,以縮短程序的執行時間。

// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

3. forEach 等數組遍歷方法的參數爲 async 函數時是併發執行的

只有 async 函數內部是繼發執行,外部不受影響,所以 forEach()map() 等數組遍歷方法的參數改爲 async 時是併發執行的。

function dbFuc() { //這裏不須要 async
  let docs = [{}, {}, {}];

  // 會獲得錯誤結果
  docs.forEach(async (doc)=> {
    await funPromise(doc);
  });
}

上面代碼會獲得錯誤結果,緣由是這時三個 funPromise(doc) 操做是併發執行的,也就是同時執行,而不是繼發執行。所以正確的寫法是採用 for 循環。

async function dbFuc() {
  let docs = [{}, {}, {}];

  for (let doc of docs) {
    await funPromise(doc);
  }
}

若是須要併發執行,可以使用 Promise.all() 方法。

async function dbFuc() {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => funPromise(doc));

  let results = await Promise.all(promises);
  return results;
}

有一組異步操做,須要按照順序完成。

async function logInOrder(urls) {
  // 併發讀取遠程URL
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });

  // 按次序輸出
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}

上面代碼中,雖然 map 方法的參數是 async 函數,但它是併發執行的,由於只有 async 函數內部是繼發執行,外部不受影響。後面的 for..of 循環內部使用了 await,所以實現了按順序輸出。

參考連接:
Promise 對象
async 函數

相關文章
相關標籤/搜索