前端戰五渣學JavaScript——Promise

前端戰五渣發表於2019-03-20

我是要成為海賊王的男人

悟空已成神,鳴人已成影,待路飛成王之時,便是我青春結束時!

悟空陪布瑪找尋龍珠,一路拳打比克、斬弗利薩,生個兒子戰沙魯,最後淨化布歐,只因承諾要保護地球。鳴人“有話直說,說到做到,這就是我的忍道”,一句會把佐助帶回來的承諾,斷臂踐行。路飛要湊齊10個船員,成為海賊王,我們相信路飛一定會成王,因為我們相信他的承諾。

我為什麼說承諾呢,今天主題不是Promise嗎,因為⬇️

promise

回撥地獄 Callback Hell

如果看這篇文章的你是有過專案經驗的,應該都遭遇過這慘絕人寰的“回撥地獄”。“回撥地獄”並不是JS或者程式語言中的一種形式,只是大家把這種程式設計中遇到的現象、問題預定俗稱的調侃成“回撥地獄”。因為只要陷進去,就很難出來。並且回撥地獄在程式碼層級上會越陷越深,邏輯看著會非常會亂,如下程式碼⬇️

// 我們用setTimeout模擬線上傳送請求等非同步執行的函式
setTimeout(() => {
    console.log(1);
    setTimeout(() => {
        console.log(2);
        setTimeout(() => {
            console.log(3);
        }, 1000)
    }, 1000) 
}, 1000);
複製程式碼

這是三個回撥函式巢狀,延遲一秒後輸出1,再過一秒輸出2,再過一秒輸出3。當然現實專案中,每個函式裡面處理的邏輯肯定不僅僅只是輸入一個數字這麼簡單,當我們回撥巢狀很多的時候,如果產品提出的一個需求我們需要更改執行順序,這個時候我們會發現巢狀邏輯複雜到難以簡單的更改順序,嚴重的只能重新寫這段的邏輯程式碼。並且回撥函式讓邏輯很不清晰。
後來就有人提出了Promise概念,這個概念意在讓非同步程式碼變得非常乾淨和直觀。

Promise 這就是我的忍道

這個概念並不是ES2015首創的,在ES2015標準釋出之前,早已有Promise/APromise/A+等概念的出現,ES2015中的Promise標準便源自於Promise/A+Promise最大的目的在於可以讓非同步函式變得竟然有序,就如我們需要在瀏覽器中訪問一個JSON座位返回格式的第三方API,在資料下載完成後進行JSON解碼,通過Promise來包裝非同步流程可以使程式碼變得非常乾淨。———————摘自《實戰ES2015》

上面最重要的一句就是可以讓非同步函式變得竟然有序,可能有人會說awaitasync也可以讓非同步函式同步執行,但是await操作符本來就是用於等待一個Promise物件的。
我們先來看一下Promise是怎麼解決上面回撥地獄這樣的難題的⬇️

// 封裝一層函式
function timeout() {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 1000)
  })
}
// 按回撥函式的邏輯執行
timeout().then(() => {
    console.log(1);
    return timeout()
}).then(() => {
    console.log(2);
    return timeout()
}).then(() => {
    console.log(3);
});
複製程式碼

我們按照回撥函式的邏輯用Promise重新寫了一遍,執行結果一樣,我們可以看出來,相比回撥函式的層級深入,使用Promise以後函式的層級明顯減少了,邏輯清晰許多。


下面我們來從頭開始認識Promise

Promise基礎

想要給一個函式賦予Promise的能力,就要先建立一個Promise物件,並將其作為函式值返回。Promise建構函式要求傳入一個函式,並帶有resovlereject引數。一個成功回撥函式,一個失敗成功回撥函式。下面是Promise物件的三個狀態:

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

三個狀態的轉換關係是從pending -> fulfilled或者pending -> rejected,並且狀態改變以後就不會再變了。pending -> fulfilled以後會去執行傳入Promise物件的resovle函式,對應的,pending -> rejected以後會去執行傳入Promise物件的reject函式。

.then()

resovle函式和reject函式是怎麼傳進去的呢,當然就是之前說的.then(),.then()可以接收兩個引數,.then(onFulfilled[, onRejected])這是官方寫法,其實就是.then(resovle, reject),第一個引數是成功回撥,第二個引數就是失敗回撥。如下⬇️

function timeout(isSuccess) {
  return new Promise((resolve, reject) => {
    if (isSuccess) {
      setTimeout(resolve, 1000)
    } else {
      reject()
    }
  })
}

timeout(true).then(() => {
  console.log('成功')
}, () => {
  console.log('失敗')
});

timeout(false).then(() => {
  console.log('成功')
}, () => {
  console.log('失敗')
});
複製程式碼

我用if語句模擬一下成功和失敗的場景,這就是.then()的用法。

.catch()

剛才說了.then()的第二個引數傳進去的是一個失敗回撥的函式,但是Promise還有一個.catch()的方法,也是用來處理失敗的,例子如下⬇️:

function timeout() {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 1000)
  })
}

timeout().then(() => {
  throw new Error('因為被凱多打敗了,所以沒當上海賊王')
}).catch((err) => {
  console.log('失敗原因:', err)
});
複製程式碼

這時候也會輸出錯誤資訊。這時候你可能會問,那.then(resovle, reject)reject.catch(reject)有什麼區別呢,下面是個人見解

.then(resovle, reject)reject.catch(reject)有什麼區別

我個人認為,.then(resovle, reject)reject按就近原則,只對最近的這個非同步函式進行錯誤處理,但是對以後的或者之前的非同步函式不做處理,而.catch(reject)會捕獲到全域性所有鏈式上非同步函式的錯誤。鏈式呼叫下面會講到。總之就是.catch(reject)管的範圍要大一些。

鏈式呼叫

Promise有一個物件鏈,並且這個物件鏈式呈流水線的模式進行作業,是因為在Promise物件對自身的onFulfilledonRejected相應器的處理中,會對其中返回的Promise物件進行處理。其中內部會將這個新的Promise物件加入到Promise物件鏈中,並將其暴露出來,使其繼續接受新的Promise物件的加入。只有當Promise物件鏈中的上一個Promise物件進入成功或者失敗階段,下一個Promise物件菜戶被啟用,這就形成了流水線的作業模式。

這就好比一開始使用Promise改造回撥地獄函式時候的樣子⬇️

// 封裝一層函式
function timeout() {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 1000)
  })
}
// 按回撥函式的邏輯執行
timeout().then(() => {
    console.log(1);
    return timeout()
}).then(() => {
    console.log(2);
    return timeout()
}).then(() => {
    console.log(3);
});
複製程式碼

可以一層一層的傳一下去,這也是厲害的地方。當鏈式呼叫中用.catch()捕獲錯誤的時候是這樣的⬇️

function timeout() {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 1000)
  })
}

timeout()
  .then(() => {
    console.log(1);
    return timeout(err)
  })
  .then(() => {
    throw new Error('發生錯誤了')
    return timeout(2)
  })
  .catch((err) => {
    console.log('123',err)
  })
  .then(() => {
    console.log(3);
  });
複製程式碼

這種情況,.catch()緊跟在丟擲錯誤的一步函式後面,會丟擲錯誤,然後繼續往下執行,但是如果.catch()是在最後,結果就完全不一樣了⬇️

function timeout() {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 1000)
  })
}

timeout()
  .then(() => {
    console.log(1);
    return timeout(err)
  })
  .then(() => {
    throw new Error('發生錯誤了')
    return timeout(2)
  })
  .then(() => {
    console.log(3);
  })
  .catch((err) => {
    console.log('123',err)
  });
複製程式碼

如果是這樣,前面說了.catch()會捕獲全域性錯誤,但是,.catch()寫在最後,丟擲錯誤以後,函式會直接跳到.catch()然後繼續往下執行,就像下面程式碼⬇️

function timeout() {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 1000)
  })
}

timeout()
  .then(() => {
    console.log(1);
    return timeout()
  })
  .then(() => {
    console.log(11);
    throw new Error('發生錯誤了')
    return timeout()
  })
  .then(() => {
    return timeout(2)
  })
  .catch((err) => {
    console.log('2',err)
  })
  .then(() => {
    throw new Error('發生錯誤了2')
    console.log(3);
  })
  .catch((err) => {
    console.log('3',err)
  });
複製程式碼

上面這段程式碼就會直接跳過輸出2的非同步函式,直接走到第一個.catch(),然後再往下執行。

Promise高階

Promise.all()

這個方法真的太實用了,比如你進入首頁,需要同時請求各種分類,使用者資訊等等資訊,我們們可能需要在所有的請求都回來以後再展示頁面,因為我們不能確定每個請求都要多久才能請求回來,所以這個問題一度很難解決。現在有了Promise.all()這個方法,真的太方便了,下面就是例子⬇️

// Promise.all()需要傳入的就是一個陣列,每一項就是每一個非同步函式
function timeout(delay) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, delay * 1000)
  })
}

Promise.all([
  timeout(1),
  timeout(3),
  timeout(5),
]).then(() => {
  console.log('都請求完畢了!')
});
複製程式碼

上面程式碼會在最大延遲的5秒後然後在執行.then()的方法,當然還有一個差不多的函式,往下看

Promise.race()

Promise.race()會監聽所有的Promise物件,在等待其中的第一個進入完成狀態的Promise物件。一旦有第一個Promise物件進入了完成狀態,該方法返回的Promise物件便會根據這第一個完成的Promise物件的狀態而改變,如下⬇️

function timeout(delay) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, delay * 1000)
  })
}

Promise.race([
  timeout(1),
  timeout(3),
  timeout(5),
]).then(() => {
  console.log('有一個請求已經結束!')
});
複製程式碼

上面程式碼在執行1秒後就會執行.then()的方法,然後剩下的兩個請求繼續等待返回。
反正我也沒遇到過什麼使用場景,知道有這個方法就行了

只管把目標定在高峰,人家要笑就讓他去笑!

寫到後面有點太官方的感覺,但是又覺得很不好解釋,只能堆例子來解釋了,跟大佬的差距還是有一定的差距,這只是基於我現在的水平到目前為止對Promise的理解。

一句承諾,就要努力去兌現。自己選擇的路,跪著也要走完。


我是前端戰五渣,一個前端界的小學生。

相關文章