[突破前端面試] —— Promise && Async/Await

前言

年前年後跳槽季,準備從面試內容入手看看前端相關知識點,旨在探究一個系列知識點,能力範圍以內的深刻探究一下。重在實踐,針對初級前端和準備面試的同窗,爭取附上實際的代碼例子以及相關試題~系列名字就用【禿破前端面試】—— 由於圈內你們共識,技術與髮量成正比。😄但願你們早日 禿 破瓶頸前端

關於面試題或者某個知識點的文章太多了,這裏筆者只是想把我的的總結用代碼倉庫的形式記錄下來並輸出文章,畢竟理論不等於實踐,知其然也要知其因此然,實踐用過才能真正理解~ git

相關係列文章:github

Promise

Promise 背景

凡事有因必有果,新事物的出現就表明着老的事物不能知足咱們的需求了。Promise 這個新事物就是在這個背景下出現的,而它代替的老事物就是ES6 以前常常被用的 callback(回調函數)。面試

雖然 ES6 Promise 已經並不能算是新事物了,可是就背景來講,它剛出現的時候確實是來解決異步回調地獄問題的。數據庫

回調地獄

什麼是回調地獄,來看一個最簡單的示例:json

setTimeout(() => {
    console.log(111);
    setTimeout(() => {
      console.log(222);
      setTimeout(() => {
        console.log(333);
        setTimeout(() => {
          console.log(444);
          // 你還能夠放置更多
          ...
        }, 4000);
      }, 3000);
    }, 2000)
  }, 1000);
複製代碼

通常來講回調地獄就是出如今異步操做中,下一次的操做依賴上一次的結果,一環套一環,套着套着就套的咱們頭痛難忍,寫出了上面的代碼。跨域

固然,上面有點爲了黑而黑了,事實上,常用的場景應該是 AJAX 請求以及數據庫的各類操做會產生回調地獄。下面代碼就是一個標準的數據庫查屢次表的一個操做(這裏我只查了兩次,可是也已經造成了嵌套)。數組

/** * 回調地獄示例 */
  const db = Object.create(null); // 假設這就是鏈接數據庫的對象
  /** * 第一步,從 A 表查出 id 爲 1 的用戶 * 第二步,從 B 表查出文章做者是 id = 1 用戶 username 的全部文章 **/
  db.query('SELECT * FROM A WHERE id = 1', function(err, results) {
    if (err) throw err;
    // 完成第一步,開始第二步
    db.query(`SELECT * FROM B WHERE author = ${results[0].username}`, function(err, results) {
      if (err) throw err;
      // 完成第二步,開始幹壞事
      console.log(results);
    });
  });
複製代碼

上面代碼,若是再繼續查下去,必定跟上面的代碼差不太多,而數據庫查詢也確實可能會出現上面的狀況。promise

Promise 解決異步避免回調地獄

出現問題了,就得解決啊,Promise 就出現了,先來看看 Promise 怎麼解決回調地獄的。bash

// promise 解決
  function f1() {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(111), 1000);
    }).then(data => console.log(data));
  }
  function f2() {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(222), 2000);
    }).then(data => console.log(data));;
  }
  function f3() {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(333), 3000);
    }).then(data => console.log(data));;
  }
  function f4() {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(444), 4000);
    }).then(data => console.log(data));;
  }
  f1().then(f2).then(f3).then(f4);
複製代碼

嗯,這麼一看,確實是解決了,並無函數嵌套,而後調用也變成了鏈式調用。固然,這個例子也有點特殊,反過來看看數據庫查詢數據的例子:

/** * 使用 Promise * 由於 Promise 是 ES6,因此下面全部代碼都使用 ES6 語法 **/
new Promise((resolve, reject) => {
    db.query('SELECT * FROM A WHERE id = 1', (err, results) => {
      if (err) reject(err);
      resolve(results);
    });
}).then(data => {
    // 拿到第一步數據,開始第二步
    db.query(`SELECT * FROM B WHERE author = ${results[0].username}`, (err, results) => {
      if (err) reject(err);
      // 完成第二步,開始幹壞事
      console.log(results);
    }); 
}).catch(err => {
    throw err;
});
複製代碼

相比之下,看起來確實要好看一些。

Promise 基礎

Promise 對象用於表示一個異步操做的最終完成 (或失敗),及其結果值。Promise 對象是一個代理對象(代理一個值),被代理的值在 Promise 對象建立時多是未知的。它容許你爲異步操做的成功和失敗分別綁定相應的處理方法(handlers)。 這讓異步方法能夠像同步方法那樣返回值,但並非當即返回最終執行結果,而是一個能表明將來出現的結果的 promise 對象。

它的出現是爲了解決 ES6 以前 JS 代碼中頻繁嵌套回調函數所致使的回調地獄問題,Promise 爲 ES6 特性。

Promise 狀態

一個 Promise 對象值是未知的,狀態是可變的,可是不管怎麼變化,它的狀態永遠處於如下三種之間:

  • pending:初始狀態,既不是成功,也不是失敗。
  • fulfilled:意味着操做成功完成。
  • rejected:意味着操做失敗。

Promise 的狀態會發生變化,成功時會從pending -> fulfilled,失敗時會從pending -> rejected,可是此過程是不可逆的,也就是不能從另外兩個狀態變成pendingfulfilled/rejected這兩個狀態也被稱爲 settled 狀態。

Promise使用

JS 萬物皆對象,因此 Promise 也能夠被咱們new出來。咱們經過下面的語法來新建一個 Promise 對象:

new Promise( function(resolve, reject) {...} /* executor */  );
複製代碼

Promise 的構造函數有一個參數 —— 是一個帶有兩個參數(resolve, reject)的函數,這兩個參數分別表明這次異步操做的結果也就是Promise的狀態。resolvereject函數被調用時,分別會將這次 Promise 的狀態改爲fulfilled或者rejected,一旦異步操做結束,Promise 的最終狀態只能是兩者之一,若是異步成功,該狀態會被resolve函數修改成fullfilled;相反當異步過程當中拋出一個錯誤,那麼該狀態就會被reject函數改爲rejected

Promise API

Promise 的原型鏈以及對象自己有一些方法供咱們使用,其中最經常使用也比較有可說性的就是下面這幾個:

then —— Promise.prototype.then(onFulfilled, onRejected)

添加解決(fulfillment)和拒絕(rejection)回調到當前 promise, 返回一個新的 promise, 將以回調的返回值來 resolve。

這麼看起來老是晦澀難懂的,仍是得實際代碼來看:

new Promise((resolve, reject) => {
    setTimeout(() => resolve(111), 1000);
  }).then(data => {
    console.log(data);
  });

  new Promise((resolve, reject) => {
    setTimeout(() => reject(111), 1000);
  }).then(data => {
    console.log(data);
  });
複製代碼

能夠看到,.then裏面拿到的是咱們 Promise resolve 事後的數據。而且他還會返回一個 Promise 繼續供咱們調用,好比:

new Promise((resolve, reject) => {
    setTimeout(() => resolve(111), 1000);
  }).then(data => {
    console.log(data); // 打印 111
    return data + 111; // 至關於 resolve(data + 111)
  }).then(data => {
    console.log(data); // 打印 222
  });
複製代碼

then()用法比較簡單,你們確定也常常用,這裏其實就知道.then()是能夠一直鏈式調用的,由於它的返回值也是一個 Promise,就能夠了。

catch -- Promise.prototype.catch(onRejected)

添加一個拒絕(rejection) 回調到當前 promise, 返回一個新的 promise。當這個回調函數被調用,新 promise 將以它的返回值來 resolve,不然若是當前 promise 進入 fulfilled 狀態,則以當前 promise 的完成結果做爲新 promise 的完成結果。

new Promise((resolve, reject) => {
    setTimeout(() => reject(111), 1000);
  }).then(data => {
    console.log('then data:', data);
  }).catch(e => {
    console.log('catch e: ', e);
  });
複製代碼

如上圖所示:一般來講,通常寫到 catch 就表示發生異常了,通常就結束了,可是從文檔說明來看,它返回的也是一個 Promise,我表示並無這麼用過,可是仍是實驗一下吧:

new Promise((resolve, reject) => {
    setTimeout(() => reject(111), 1000);
  }).then(data => {
    console.log('then data:', data);
  }).catch(e => {
    console.log('catch e: ', e);
    return e;
  }).then(data => {
    console.log('catch data: ', data);
  });
複製代碼

好吧,漲姿式了,可是仍是那句話,我的以爲 catch 到錯誤就能夠了,不必下一步了,除非你還要用錯誤作其餘的事情~

finally —— Promise.prototype.finally()

上面提到了catch()通常來講用於捕獲錯誤,因此大部分代碼應該是到這一步就結束了,可是實際上 Promise 提供了標準結束方法 finally(),只要 Promise 狀態變成 settled,不管是 rejected 仍是 fulfilled,都會在 finally 裏捕獲。

new Promise((resolve, reject) => {
    setTimeout(() => reject(111), 1000);
  }).then(data => {
    console.log('then data:', data);
  }).catch(e => {
    console.log('catch e: ', e);
    return e;
  }).then(data => {
    console.log('catch data: ', data);
    return data;
  }).finally(() => {
    console.log('promise finally');
    return 222;
  }).then(data => {
    console.log('finally data: ', data);
  });
複製代碼

從上圖能夠看得出,finally 也會返回一個 promise,可是我勸你們善良,真的到 finally 就能夠結束了!!!這裏只是爲了演示它的返回。

我想了一下,不經常使用的緣由多是本身太 low 了,其實它仍是有很明顯的試用場景的。好比官方給出的Demo:

let isLoading = true;

fetch(myRequest).then(function(response) {
    var contentType = response.headers.get("content-type");
    if(contentType && contentType.includes("application/json")) {
      return response.json();
    }
    throw new TypeError("Oops, we haven't got JSON!");
  })
  .then(function(json) { /* process your JSON further */ })
  .catch(function(error) { console.error(error); /* this line can also throw, e.g. when console = {} */ })
  .finally(function() { isLoading = false; });
複製代碼

這個場景應該在實際開發過程當中很經常使用,若是不使用 finally,咱們會在 then 和 catch 裏分別設置一次isLoading = false;,而使用 finally 則只須要賦值一次,不只避免了重複代碼並且優化了邏輯~這纔是正確的使用之道啊~

Promise.finally(fn)須要注意如下兩點:

  • 參數 fn 是一個無參函數,不論該 promise 最終是 fulfilled 仍是 rejected。
  • finally 不會改變 promise 的狀態。

all —— Promise.all(iterable)

這個方法返回一個新的 promise 對象,該 promise 對象在 iterable 參數對象裏全部的 promise 對象都成功的時候纔會觸發成功,一旦有任何一個 iterable 裏面的 promise 對象失敗則當即觸發該 promise 對象的失敗。這個新的 promise 對象在觸發成功狀態之後,會把一個包含 iterable 裏全部 promise 返回值的數組做爲成功回調的返回值,順序跟 iterable 的順序保持一致;若是這個新的 promise 對象觸發了失敗狀態,它會把 iterable 裏第一個觸發失敗的 promise 對象的錯誤信息做爲它的失敗錯誤信息。Promise.all 方法常被用於處理多 個promise 對象的狀態集合。

這個算是我常用的一個 API 了,上面的內容雖然有點長,可是總結起來其實也很簡單,大概就是以下三個方面:

  • 第一:接收一個 Promise 對象數組做爲參數
// promise 解決
  function f1() {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(111), 1000);
    }).then(data => console.log(data));
  }
  function f2() {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(222), 2000);
    }).then(data => console.log(data));;
  }
  function f3() {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(333), 3000);
    }).then(data => console.log(data));;
  }

  Promise.all([f1, f2, f3]);
複製代碼

  • 第二:參數全部回調成功纔是成功,返回值數組與參數順序一致
// promise 解決
  function f1() {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(111), 1000);
    });
  }
  function f2() {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(222), 2000);
    });;
  }
  function f3() {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(333), 3000);
    });;
  }

  Promise.all([f1(), f2(), f3()]).then(results => {
    console.log(results);
  });
複製代碼

能夠看到,返回值是一個數組,而且每一個元素對應的就是參數數組裏對應事後的resolve值。

  • 第三:參數數組其中一個失敗,則觸發失敗狀態,第一個觸發失敗的 Promise 錯誤信息做爲 Promise.all 的錯誤信息。
// promise 解決
  function f1() {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(111), 1000);
    });
  }
  function f2() {
    return new Promise((resolve, reject) => {
      setTimeout(() => reject(222), 2000);
    });;
  }
  function f3() {
    return new Promise((resolve, reject) => {
      setTimeout(() => reject(333), 3000);
    });;
  }

  Promise.all([f1(), f2(), f3()]).then(results => {
    console.log(results);
  }).catch(e => {
    console.log(e);
  });
複製代碼

能夠看到,當我把第二個和第三個分別設置成 reject 的時候,Promise.all 進入了 catch 也就是捕獲異常階段,捕獲到的是第二個 reject 內容,也就是第一次出現的 reject 的那個地方。

因此,通常來講,Promise.all 用來處理多個併發請求,也是爲了頁面數據構造的方便,將一個頁面所用到的在不一樣接口的數據一塊兒請求過來,不過,若是其中一個接口失敗了,多個請求也就失敗了,頁面可能啥也出不來,這就看當前頁面的耦合程度了~

race

當 iterable 參數裏的任意一個子 promise 被成功或失敗後,父 promise 立刻也會用子 promise 的成功返回值或失敗詳情做爲參數調用父 promise 綁定的相應句柄,並返回該 promise 對象。

這個 API 講道理,不常用,可是在某些場景下,仍是特別給力的。怎麼說的,字面意義就是競賽,想象一個場景,用戶登陸和取消,登陸過程是一個請求過程,會耗時,假設我這邊點擊登陸以後,數據請求過程當中點擊了取消,那麼若是登陸還未響應回來,應該就是取消這個行爲贏得了競爭,就不登陸了。

固然,登陸取消這個場景我沒有實際使用過,我只在一個地方用到過 Promise.race —— fetch timeout,衆所周知,前端若是使用 fetch 請求的時候,沒辦法設置超時時間,由於 fetch 內部並無 timeout 這個參數,那麼若是咱們但願前端能夠設置超時時間,好比超過5s沒有響應數據的話就認爲請求超時了,這個時候可使用 Promise.race 來幫助咱們實現。由於 fetch 本質上也是 Promise,咱們只須要在 Promise.race 裏將 fetch 和一個 5s 延時事後 reject/resolve 的 Promise 進行競賽便可。具體代碼以下:

// fetch timeout實現
timeoutPromise = () => new Promise((resolve) => {
  setTimeout(() => {
    resolve(
      new Response(
        'Timeout',
        {
          status: 408,
          statusText: "Fetch timeout",
          ok: false
        }
      )
    );
  }, timeout = 5000);
});

Promise.race([timeoutPromise(), fetch(url, opts)])
  .then(res => res.json())
  .then(data => {
    return data;
  });
複製代碼

由於我比較喜歡用 fetch,因此剛好有這個場景的使用,親測可用,具體細節內容你們能夠根據本身的項目去修改,這裏不過多介紹,感興趣能夠留言交流。

手寫一個 Promise

講到這裏,必定會有人問了,是否是又要手寫一個 Promise 了?固然不會! 我說過了,重在實踐,從實踐角度出發,我以爲並不會有人在項目裏使用本身手寫的 Promise 而是都直接 new Promise(),所以,我再去畫蛇添足浪費本身和你們的時間去寫一個並不會有人用的 Promise,也沒什麼意義,若是大家想了解內部實現,建議直接去看源碼~

Promise 源碼地址

Async/Await

還得再來一遍,新事物的出現就表明着老的事物不能知足咱們的需求,ES6 剛出 Promise 來解決異步問題,ES7 就又出了一個 Async/Await(其實官方名字是 async function),看來 Promise 並無達到你們夥的預期,因此官方就又搞了個更爲優雅的異步解決方案。

爲何說它是爲了解決 Promise 帶來的問題,能夠看看 MDN 官網的下面這段話:

async/await 的目的是簡化使用多個 promise 時的同步行爲,並對一組 Promises 執行某些操做。正如 Promises 相似於結構化回調,async/await 更像結合了 generators 和 promises。

Promise 並非完美的解決方案

上面提到的那個異步嵌套 setTimeout的例子來講,事實上,大部分人用 Promise 應該並不會像上面的代碼那樣寫,而是下面這樣:

/* Async/Await */
 new Promise((resolve, reject) => {
    setTimeout(() => resolve(111), 1000);
  }).then(data => {
    console.log(data);
    new Promise((resolve, reject) => {
      setTimeout(() => resolve(222), 2000)
    }).then(data => {
      console.log(data);
      new Promise((resolve, reject) => {
        setTimeout(() => resolve(333), 3000)
      }).then(data => {
        console.log(data);
        new Promise((resolve, reject) => {
          setTimeout(() => resolve(444), 4000)
        }).then(data => {
          console.log(data);
        })
      })
    })
  });
複製代碼

嗯,說實話,其實 Promise.then() 若是使用過多,依然仍是回調地獄,嵌套依然沒有消失,因此來講,Promise 並不能稱之爲完美的異步方案,所以,ES7 提出了 async function,它用來更爲優雅的解決異步。咱們此次就來看看它的魅力:

// 定時器嵌套
  function f1() {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(111), 1000);
    }).then(data => console.log(data));
  }
  function f2() {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(222), 2000);
    }).then(data => console.log(data));;
  }
  function f3() {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(333), 3000);
    }).then(data => console.log(data));;
  }
  function f4() {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(444), 4000);
    }).then(data => console.log(data));;
  }

  async function timeoutFn() {
    await f1(); // 開始執行第一個異步函數
    await f2(); // 第一個執行完,開始執行第二個異步函數
    await f3(); // 第二個執行完,開始執行第三個異步函數
    await f4(); // 第三個執行完,開始執行第四個異步函數
  }
  timeoutFn();
複製代碼
// 數據庫查詢
async function queryData() {
    try {
      // 第一步,獲取數據
      const step1Data = await db.query('SELECT * FROM A WHERE id = 1');
      // 第二步,獲取數據
      const step2Data = await db.query(`SELECT * FROM B WHERE author = ${step1Data[0].username}`);
      console.log(step2Data);
    } catch(e) {
      throw e;
    }
}
複製代碼

看看上面的代碼,多麼的優美,徹底的同步流程~稱之爲最完美異步解決方案一點也不爲過。

async function 基礎

關於 async function,其實並無過多的 API,由於它更像是一個高級語法糖,官方文檔給出的也更多都是使用示例。在這裏,其實咱們只須要知道並強調一件事 —— await 關鍵字用來暫停等待異步函數的執行結束,若是是 Promise,也就是等待它的 settled 狀態,而且 await 只能出如今 async function 內部,不可單獨使用

示例

官方給出了一個比較有意思的例子:

// 一個1秒的異步函數
var resolveAfter1Second = function() {
  console.log("starting fast promise");
  return new Promise(resolve => {
    setTimeout(function() {
      resolve("fast");
      console.log("fast promise is done");
    }, 1000);
  });
};

// 一個2秒的異步函數
var resolveAfter2Seconds = function() {
  console.log("starting slow promise");
  return new Promise(resolve => {
    setTimeout(function() {
      resolve("slow");
      console.log("slow promise is done");
    }, 2000);
  });
};

// 下面這種寫法是一塊兒執行異步函數,只不過由於await等待致使輸出有前後
var concurrentStart = async function() {
  console.log('==CONCURRENT START with await==');
  const slow = resolveAfter2Seconds(); // starts timer immediately
  const fast = resolveAfter1Second(); // starts timer immediately

  // 1. Execution gets here almost instantly
  console.log(await slow); // 2. this runs 2 seconds after 1.
  console.log(await fast); // 3. this runs 2 seconds after 1., immediately after 2., since fast is already resolved
}

// 下面這種是標準的等待寫法
var sequentialStart = async function() {
  console.log('==SEQUENTIAL START==');

  // 1. Execution gets here almost instantly
  const slow = await resolveAfter2Seconds();
  console.log(slow); // 2. this runs 2 seconds after 1.

  const fast = await resolveAfter1Second();
  console.log(fast); // 3. this runs 3 seconds after 1.
}
複製代碼

具體來講你們能夠本身實際體驗一下,第二種沒什麼可說的,想象中就是這個樣子,由於 await 會暫停等待函數執行完以後再向下執行,所以等待時間不會重疊,先等待2秒執行 slow 後再等待1秒執行 fast。

而第一種

const slow = resolveAfter2Seconds();
const fast = resolveAfter1Second();

console.log(await slow);
console.log(await fast);
複製代碼

上面這兩個異步函數由於沒有 await 關鍵字,都是當即執行,所以先輸出promise start,以後,兩個函數延時不一樣,雖然 slow 先執行,可是是2秒,而 fast 後執行是1秒,先輸出fast done再輸出slow done。最後,await 關鍵字發揮做用,雖然 fast 先執行完,可是你仍是要等 await slow 完事以後才能 await fast。

總結

這裏就不給相關面試題了,把背景和基礎內容都瞭解了,API 都知道如何使用了,那麼面試題也就百變不離其宗了,也沒什麼可說的了。寫到此處突然想起來一個問題,那麼仍是說一下吧。setTimeout 和 Promise 都是異步操做,那麼誰更快呢?

function whoFast() {
  setTimeout(() => console.log('settimeout'), 0);
  new Promise(() => {
    console.log('promise');
  })
}

複製代碼

實踐是檢驗真理的惟一標準,promise 無關順序更快執行,至於原理,你們就去看 js 的 event loop 機制吧,若是感興趣,後續也能夠寫~

代碼地址

補充

前面幾篇我的以爲寫得很好的沒啥人看,這一篇感受也沒寫什麼竟然不少人評論,確實沒想到,因此有一些細節並無考慮到,😄。在這裏進行補充:

補充一: Promise.allSettled(iterable)

上面提到了,Promise.all([])若是出現異常則會直接返回第一個錯誤,那麼即便有的成功了也不會返回,這樣作有時候會出現問題,一個頁面兩個接口,使用Promise.all()來獲取,若是一個成功一個失敗你至少應該把成功那個展現纔對,嗯,因此這時候就用到了Promise.allSettled(),它返回的也是一個對應數組,裏面是對應 Promise 的 setteld 狀態,可能成功,也可能失敗~

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));
const promises = [promise1, promise2];

Promise.allSettled(promises).
  then((results) => results.forEach((result) => console.log(result.status)));

// expected output:
// "fulfilled"
// "rejected"
複製代碼

補充二: await 後面接同步代碼會如何?

直接上圖上面說過,await 是等待異步代碼執行結束,後面通常都會跟異步函數,可是若是你就是要跟同步代碼會怎麼樣呢?不要緊,上圖你也能看得出,跟同步代碼,await 同步代碼依然會轉換成 Promise~

參考文章

相關文章
相關標籤/搜索