【前端】從小白視角上手Promise、Async/Await和手撕代碼
寫在前面
對于前端新手而言,Promise是一件比較困擾學習的事情,需要理解的細節比較多。對于前端面試而言,Promise是面試官最常問的問題,特別是手撕源碼。眾所周知,JavaScript語言執行環境是“單線程”。
單線程,就是指一次只能完成一件任務,如果有多個任務就必須排隊等候,前面一個任務完成,再執行后面一個任務。這種“單線程”模式執行效率較低,任務耗時長。
為了解決這個問題,就有了異步模式,也叫異步編程。
一、異步編程
所謂"異步",簡單說就是一個任務分成兩段,先執行第一段,然后轉而執行其他任務,當第一段有了執行結果之后,再回過頭執行第二段。
JavaScript采用異步編程原因有兩點:
- JavaScript是單線程。
- 為了提高CPU的利用率。
在提高CPU的利用率的同時也提高了開發難度,尤其是在代碼的可讀性上。
那么異步存在的場景有:
- fs 文件操作
- require("fs").readFile("./index.html",(err,data)=>{})
- 數據庫操作
- AJAX
- $.get("/user",(data)=>{})
定時器
- setTimeout(()=>{},2000)
二、Promise是什么
Promise理解
(1) 抽象表達
- Promise 是一門新的技術(es6規范)
- Promise是js中進行異步編程的新解決方案
(2) 具體表達
- 從語法上說:Promise是一個構造函數
- 從功能上說:Promise對象是用來封裝一個異步操作并可以獲取其成功/失敗的結果值
為什么要使用Promise
(1) 指定回調函數的方式更加靈活
- promise:啟動異步任務=>返回promise對象=>給promise對象綁定回調函數
(2) 支持鏈式調用方式,可以解決回調地獄問題
- 什么是回調地獄?
回調地獄就是回調函數嵌套使用,外部回調函數異步執行的結果是嵌套的回調執行的條件
- 回調地獄的缺點
不便于閱讀
不便于異常處理
解決方法
Promise的狀態
- Promise必須擁有三種狀態:pending、rejected、resolved
- 如果Promise的狀態是pending時,它可以變成成功fulfilled或失敗rejected
- 如果promise是成功狀態,則它不能轉換為任何狀態,而且需要一個成功的值,并且這個值不能改變
- 如果promise是失敗狀態,則它不能轉換成任何狀態,而且需要一個失敗的原因,并且這個值不能改變
Promise的狀態改變
pending未決定的,指的是實例狀態內置的屬性
(1)pending變為resolved/fullfilled
(2)pending變為rejected
說明:Promise的狀態改變只有兩種,且一個Promise對象只能改變一次,無論失敗還是成功都會得到一個結果輸出,成功的結果一般是value,失敗的結果一般是reason。
無論狀態是成功還是失敗,返回的都是promise。
Promise的值
實例對象中的另一個屬性 [PromiseResult]保存著異步任務 [成功/失敗] 的結果resolve/reject。
Promise的api
手寫Promide中的api:
(1)promise構造函數 Promise(executor){}
- executor:執行器(resolve,reject)=>{}
- resolve:內部定義成功時我們需要調用的函數value=>{}
- reject:內部定義失敗時我們調用的函數 reason=>{}說明:executor會在Promise內部立即同步調用,異步操作在執行器中執行
(2)Promise.prototype.then方法:(onResolved,rejected)=>{}
- onResolved函數:成功的回調函數value=>{}
- rejected函數:失敗的回調函數reason=>{}
說明:指定用于得到成功value的成功回調和用于得到失敗reason的失敗回調,返回一個新的promise對象
(3)Promise.prototype.catch方法:(onRejected)=>{}
前三條是本文章中將要實現的手寫代碼,當然Promise還有其它的api接口。
(1)Promise.prototype.finally()方法
finally()方法用于指定不管 Promise 對象最后狀態如何,都會執行的操作。不管promise最后的狀態,在執行完then或catch指定的回調函數以后,都會執行finally方法指定的回調函數。
- promise
- .then(result => {···})
- .catch(error => {···})
- .finally(() => {···});
(2)Promise.all()方法
Promise.all()方法用于將多個 Promise 實例,包裝成一個新的 Promise 實例。
- const p = Promise.all([p1, p2, p3]);
p的狀態由p1、p2、p3決定,分成兩種情況。
- 只有p1、p2、p3的狀態都變成fulfilled,p的狀態才會變成fulfilled,此時p1、p2、p3的返回值組成一個數組,傳遞給p的回調函數。
- 只要p1、p2、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。
(3)Promise.race()方法
Promise.race()方法同樣是將多個 Promise 實例,包裝成一個新的 Promise 實例。
- const p = Promise.race([p1, p2, p3]);
只要p1、p2、p3之中有一個實例率先改變狀態,p的狀態就跟著改變。那個率先改變的 Promise 實例的返回值,就傳遞給p的回調函數。
(4)Promise.allSettled()方法
Promise.allSettled()方法接受一組 Promise 實例作為參數,包裝成一個新的 Promise 實例。只有等到所有這些參數實例都返回結果,不管是fulfilled還是rejected,包裝實例才會結束。
- const promises = [
- fetch('/api-1'),
- fetch('/api-2'),
- fetch('/api-3'),
- ];
- await Promise.allSettled(promises);
- removeLoadingIndicator();
該方法返回的新的 Promise 實例,一旦結束,狀態總是fulfilled,不會變成rejected。狀態變成fulfilled后,Promise 的監聽函數接收到的參數是一個數組,每個成員對應一個傳入Promise.allSettled()的 Promise 實例。
(5)Promise.any()方法
ES2021 引入了Promise.any()方法。該方法接受一組 Promise 實例作為參數,包裝成一個新的 Promise 實例返回。只要參數實例有一個變成fulfilled狀態,包裝實例就會變成fulfilled狀態;如果所有參數實例都變成rejected狀態,包裝實例就會變成rejected狀態。
(6)Promise.reject(reason)方法
Promise.reject(reason)方法也會返回一個新的 Promise 實例,該實例的狀態為rejected。
- const p = Promise.reject('出錯了');
- // 等同于
- const p = new Promise((resolve, reject) => reject('出錯了'))
- p.then(null, function (s) {
- console.log(s)
- });
- // 出錯了
(7)Promise.resolve()方法 有時需要將現有對象轉為 Promise 對象,Promise.resolve()方法就起到這個作用。
- Promise.resolve('foo')
- // 等價于
- new Promise(resolve => resolve('foo'))
改變promsie狀態和指定回調函數誰先誰后?
(1)都有可能,正常情況下是先指定回調函數再改變狀態,但也可以先改變狀態再指定回調函數。
(2)如何改變狀態再指定回調?
- 在執行器中直接調用resolve/reject
- 延遲更長時間才進行調用then
(3)什么時候才能得到數據?
- 如果先指定的回調,那當狀態發生改變,回調函數就會調用,得到數據
- 如果先改變的狀態,那當指定回調時,回調函數就會進行調用,得到數據
示例:
- let p = new Promise((resolve,reject)=>{
- resolve("成功了");
- reject("失敗了");
- });
- p.then((value)=>{
- console.log(value);
- },(reason)=>{
- console.log(reason);
- })
Promise規范
- "Promise"是一個具有then方法的對象或函數,其行為符合此規范。也就是說Promise是一個對象或函數
- "thenable"是一個具有then方法的對象或函數,也就是這個對象必須擁有then方法
- "value"是任何合法的js值(包括undefined或promise)
- promise中的異常需要使用throw語句進行拋出
- promise失敗的時候需要給出失敗的原因
then方法說明
- 一個promise必須要有一個then方法,而且可以訪問promise最終的結果,成功或者失敗的值
- then方法需要接收兩個參數,onfulfilled和onrejected這兩個參數是可選參數
- promise無論then方法是否執行完畢,只要promise狀態變了,then中綁定的函數就會執行。
鏈式調用
Promise最大的優點就是可以進行鏈式調用,如果一個then方法返回一個普通值,這個值就會傳遞給下一次的then中,作為成功的結果。
如果返回的是一個promise,則會把promise的執行結果傳遞下去取決于這個promise的成功或失敗。
如果返回的是一個報錯,就會執行到下一個then的失敗函數中。
三、手寫Promise代碼
面試經常考的手寫Promise代碼,可以仔細理解一下。
- // 手寫Promise
- // 首先定義一個構造函數,在創建Promise對象的時候會傳遞一個函數executor,
- // 這個函數會立即被調用,所以我們在Promise內部立即執行這個函數。
- function Promise(executor){
- // 用于保存promise的狀態
- this.status = "pending";
- this.value;//初始值
- this.reason;//初始原因
- this.onResolvedCallbacks = [];//存放所有成功的回調函數
- this.onRejectedCallbacks = [];//存放所有失敗的回調函數
- //定義resolve函數
- const resolve = (value)=>{
- if(this.status === "pending"){
- this.status = "resolved";
- this.value = value;
- this.onResolvedCallbacks.forEach(function(fn){
- fn()
- })
- }
- }
- //定義reject函數
- const reject = (reason)=>{
- if(this.status === "pending"){
- this.status = "rejected";
- this.reason = reason;
- this.onRejectedCallbacks.forEach(function(fn){
- fn()
- })
- }
- }
- executor(resolve,reject);
- }
- Promise.prototype.then = function(onFulfilled,onRejected){
- /*
- 每次then都會返回一個新的promise
- 我們需要拿到當前then方法執行成功或失敗的結果,
- 前一個then方法的返回值會傳遞給下一個then方法,
- 所以這里我們要關心onFulfilled(self.value)
- 和 onRejected(self.reason)的返回值,我們這里定義一個x來接收一下。
- 如果失敗拋錯需要執行reject方法,這里使用try...catch捕獲一下錯誤。
- 也就是判斷then函數的執行結果和返回的promise的關系。
- */
- return new Promise((resolve,reject)=>{
- //當Promise狀態為resolved時
- if(this.status === "resolved"){
- try{
- resolve(onFulfilled(this.value))
- }catch(error){
- reject(error)
- }
- }
- //當Promise狀態為rejected時
- if(this.status === "rejected"){
- try {
- resolve(onRejected(this.reason))
- } catch (error) {
- reject(error)
- }
- }
- //當Promise狀態為pendding
- if(this.status === "pending"){
- this.onResolvedCallbacks.push(function(){
- try{
- resolve(onFulfilled(this.value))
- }catch(error){
- reject(error)
- }
- });
- this.onRejectedCallbacks.push(function(){
- try {
- resolve(onRejected(this.reason))
- } catch (error) {
- reject(error)
- }
- });
- }
- })
- }
升級版Promise:
- class Promise{
- /*首先定義一個構造函數,在創建Promise對象的時候會傳遞一個函數executor,
- 這個函數會立即被調用,所以我們在Promise內部立即執行這個函數。*/
- constructor(executor){
- this.executor = executor(this.resolve,this.reject);
- this.onResolvedCallbacks = [];//存放所有成功的回調函數
- this.onRejectedCallbakcs = [];//存放所有失敗的回調函數
- }
- // 用于存儲相應的狀態
- status = "pending";
- // 初始值
- value;
- // 初始原因
- reason;
- // executor在執行的時候會傳入兩個方法,一個是resolve,
- // 一個reject,所以我們要創建這兩個函數,而且需要把這兩個函數傳遞給executor。
- // 當我們成功或者失敗的時候,執行onFulfilled和onRejected的函數,
- // 也就是在resolve函數中和reject函數中分別循環執行對應的數組中的函數。
- // 定義成功事件
- resolve(value){
- if(status === "pending"){
- status = "resolved";
- value = value;
- this.onResolvedCallbacks.forEach(fn=>{fn()})
- }
- }
- // 定義失敗事件
- reject(){
- if(this.status === "pending"){
- this.status = "rejected";
- this.reason = reason;
- this.onRejectedCallbakcs.forEach(fn=>{fn()});
- }
- }
- // 這個時候當我們異步執行resolve方法時候,then中綁定的函數就會執行,并且綁定多個then的時候,多個方法都會執行。
- // Promise的對象存在一個then方法,這個then方法里面會有兩個參數,一個是成功的回調onFulfilled,
- // 另一個是失敗的回調onRejected,只要我們調用了resolve就會執行onFulfilled,調用了reject就會執行onRejected。
- // 為了保證this不錯亂,我們定義一個self存儲this。當我們調用了resolve或reject的時候,需要讓狀態發生改變.
- // 需要注意的是Promise的狀態只可改變一次,所以我們要判斷,只有當狀態未發生改變時,才去改變狀態。
- then(onFulfilled,onRejected){
- // 判斷當前狀態進行回調
- if(this.status === "resolved"){
- onFulfilled(self.value)
- };
- if(this.status === "rejected"){
- onRejected(self.reason)
- }
- // 當狀態還處于pending狀態時
- // 因為onFulfilled和onRejected在執行的時候需要傳入對應的value值,所我們這里用一個函數包裹起來,將對應的值也傳入進去。
- if(this.status === "pending"){
- this.onResolvedCallbacks.push(()=>{onFulfilled(this.value)});
- this.onResolvedCallbacks.push(()=>{onRejected(this.reason)});
- }
- }
- }
使用自己手寫的Promise源碼:
- let p = new Promise((resolve,reject)=>{
- setTimeout(()=>{
- resolve("成功了")
- },1000)
- });
- p.then(function(value){
- return 123;
- }).then(value=>{
- console.log("收到了成功的消息:",value);
- }).catch(error=>{
- console.log(error);
- });
- p.then(value=>{
- console.log(value);
- })
四、Async/Await
async用來表示函數是異步的,定義的async函數返回值是一個promise對象,可以使用then方法添加回調函數。
await 可以理解為是 async wait 的簡寫。await 必須出現在 async 函數內部,不能單獨使用。函數中只要使用await,則當前函數必須使用async修飾。
所以回調函數的終結者就是async/await。
async命令
- async函數返回的是一個promise對象。
- async函數內部return語句返回的值,會成為then方法回調的參數。
- async函數內部拋出錯誤,會導致返回的 Promise 對象變為reject狀態。
- 拋出的錯誤對象會被catch方法回調函數接收到。
async函數返回的 Promise 對象,必須等到內部所有await命令后面的 Promise 對象執行完,才會發生狀態改變,除非遇到return語句或者拋出錯誤。
也就是說,只有async函數內部的異步操作執行完,才會執行then方法指定的回調函數。
- async function fun(){
- // return "hello wenbo";
- throw new Error("ERROR");
- }
- fun().then(v => console.log(v),reason=>console.log(reason));//Error: ERROR```
await命令
正常情況下,await命令后面是一個 Promise 對象,返回該對象的結果。如果不是 Promise 對象,就直接返回對應的值。
- async function fun(){
- return await "zhaoshun";
- // 等價于 return "zhaoshun";
- }
- fun().then(value=>console.log(value));//zhaoshun
另一種情況是,await命令后面是一個thenable對象(即定義了then方法的對象),那么await會將其等同于 Promise 對象。
- class Sleep{
- constructor(timeout){
- this.timeout = timeout;
- }
- then(resolve,reject){
- const startTime = Date.now();
- setTimeout(()=>resolve(Date.now() - startTime),this.timeout);
- }
- }
- (
- async ()=>{
- const sleepTime = await new Sleep(1000);
- console.log(sleepTime);//1012
- }
- )()
- // js里面沒有休眠的語法,但是借助await命令可以讓程序停頓的時間
- const sleepFun = (interval) => {
- return new Promise(resolve=>{
- setTimeout(resolve,interval);
- })
- }
- // 用法
- const asyncFun = async ()=>{
- for(let i = 1; i <= 5; i++){
- console.log(i);
- await sleepFun(1000);
- }
- }
- asyncFun();
從上面可以看到,await命令后面是一個Sleep對象的實例。這個實例不是 Promise 對象,但是因為定義了then方法,await會將其視為Promise處理。
await命令后面的 Promise 對象如果變為reject狀態,則reject的參數會被catch方法的回調函數接收到。
注意:上面代碼中,await語句前面沒有return,但是reject方法的參數依然傳入了catch方法的回調函數。這里如果在await前面加上return,效果是一樣的。
- 任何一個await語句后面的 Promise 對象變為reject狀態,那么整個async函數都會中斷執行。
- 使用try/catch可以很好處理前面await中斷,而后面不執行的情況。
示例:
- const fun = async ()=>{
- try {
- await Promise.reject("ERROR");
- } catch (error) {
- }
- return await Promise.resolve("success");
- }
- fun().then(
- value=>console.log(value),reason=>console.log(reason,"error")//
- ).catch(
- error=>console.log(error)//ERROR
- );
另一種方法是await后面的 Promise 對象再跟一個catch方法,處理前面可能出現的錯誤。
- const fun = async ()=>{
- await Promise.reject("error").catch(e=>console.log(e));
- return await Promise.resolve("success");
- }
- fun().then(v=>console.log(v));//success
錯誤處理
第一點:如果await后面的異步操作出錯,那么等同于async函數后面的promise對象被reject。
- const fun = async()=>{
- await new Promise((resolve,reject)=>{
- throw new Error("error")
- })
- }
- fun().then(v=>console.log(v)).catch(e=>console.log(e));
第二點:多個await命令后面的異步操作,如果不存在繼發關系,最好讓他們同時進行觸發。
- const [fun1,fun2] = await Promise.all([getFun(),getFoo()]);
- const fooPromise = getFoo();
- const funPromise = getFun();
- const fun1 = await fooPromise();
- const fun2 = await funPromise();
第三點:await命令只能用在async函數之中,如果用在普通函數,就會報錯。
- async function dbFuc(db) {
- let docs = [{}, {}, {}];
- // 報錯
- docs.forEach(function (doc) {
- await db.post(doc);
- });
- }
第四點:async 函數可以保留運行堆棧。
小結在這篇文章中我們總結了異步編程和回調函數的解決方案Promise,以及回調終結者aysnc/await。
- Promise有三個狀態:pending、rejected、resolved。
- Promise的狀態改變,只能改變一次,只有兩種改變:pending變為resolved/fullfilled、pending變為rejected。
- Promise最大優點就是可以進行鏈式調用。
- async用來表示函數是異步的,定義的async函數返回值是一個promise對象。
- 函數中只要使用await,則當前函數必須使用async修飾。
- 回調函數的終結者就是async/await。
參考文章
- 《異步終結者 async await,了解一下》
- 《一次性讓你懂async/await,解決回調地獄》
- 《面試精選之Promise》
- 《BAT前端經典面試問題:史上最最最詳細的手寫Promise教程》
- 《Promise實現原理(附源碼)》
- 《JavaScript高級程序設計(第四版)》
- 《你不知道的JavaScript(中卷)》
- 阮一峰《ES6 入門教程》
本文轉載自微信公眾號「前端萬有引力」,可以通過以下二維碼關注。轉載本文請聯系前端萬有引力公眾號。