快點上車,前端異步編程發(fā)車了
本文轉(zhuǎn)載自微信公眾號「零零后程序員小三」,作者003 。轉(zhuǎn)載本文請聯(lián)系零零后程序員小三公眾號。
什么是異步編程?
異步編程允許我們在執(zhí)行一個長時間任務(wù)的時候,程序不用進行等待就可以繼續(xù)執(zhí)行后面的代碼,直到任務(wù)完成后再以回調(diào)函數(shù)(callback)的方式回頭通知你
這種編程模式避免了程序的阻塞,提高了效率,它適用于那些網(wǎng)絡(luò)請求或者數(shù)據(jù)庫操作的應(yīng)用。
實現(xiàn)異步的方式
回調(diào)函數(shù),是最簡單的實現(xiàn)異步的方式
- console.log('111');
- setTimeout(() => {
- console.log("222");
- }, 2000)
- console.log('333');
雖然,按照html文檔的輸出規(guī)則他是自上而下,但是在中間加了一個定時器,然后瀏覽器識別到了它,會馬上執(zhí)行,然后執(zhí)行后面的代碼,等到了給定時間,會以回調(diào)函數(shù)的方式返回。
在JS的設(shè)計之初,他一開始就是單線程的編程語言,盡管這里回調(diào)函數(shù)看上去和主線程一起進行的,但是都運行在一個線程中,況且主線程還運行其他代碼。
雖然JS只有一個線程,但是還是有比較不錯的優(yōu)點的。因為所有的操作都在一個線程之中,所以不用去考慮資源競爭的問題,而且在源頭就避開了線程之間的頻繁切換,從而降低了線程開銷
但是它也有一個致命的缺點,如果我們需要進行多個異步操作,我們可能會寫出下面的代碼
- console.log("111");
- setTimeout(() => {
- console.log("三秒后執(zhí)行1");
- setTimeout(() => {
- console.log("三秒后執(zhí)行2");
- setTimeout(() => {
- console.log("三秒后執(zhí)行3");
- setTimeout(() => {
- console.log("三秒后執(zhí)行4");
- }, 3000)
- }, 3000);
- }, 3000);
- }, 3000);
- console.log("333");
如果再有別的回調(diào),這樣會更恐怖,一直寫下去,換誰都看得心慌。我們管這個叫回調(diào)地獄
解決回調(diào)地獄
為了解決這個回調(diào)地獄,Promise誕生了。
我們在頁面中動態(tài)的更新數(shù)據(jù),也就是AJAX技術(shù),就是使用Promise的API fetch()實現(xiàn)的
我們可以試一試用fetch()獲取一個接口的數(shù)據(jù)
通過運行可知道他返回的是一個Promise對象,但是我們還沒有獲得我們想要的數(shù)據(jù),因為Promise翻譯一下就是承諾的意思,所以,他應(yīng)該會在后來給我們實現(xiàn)我們想要的需求,所以,我在后面加個then,then翻譯一下就也是然后的意思
所以就是傳入它的then方法并傳入一個回調(diào)函數(shù),如果在后來這個請求成功之后,然后回調(diào)函數(shù)會被調(diào)起,請求的函數(shù)會被作為一個參數(shù)傳入
- fetch("http://jsonplaceholder.typicode.com/posts/1")
- .then((response)=> ...)
但是如果這樣看來Promise和回調(diào)函數(shù)就沒有區(qū)別了。
但是,Promise的優(yōu)點在于它可以用一種鏈式結(jié)構(gòu)將多個異步操作串聯(lián)起來
也就是 比如下面的response.json()方法也會返回一個Promise,然后then之后就是將未來返回的response轉(zhuǎn)換為json格式,
- fetch("http://jsonplaceholder.typicode.com/posts/1")
- .then((response)=>response.json())
然后我們還可以繼續(xù)追加我們想要進行的操作,直接then下去,比如下面這樣把結(jié)果打印出來或者把結(jié)果存到某個容器中等
- fetch("http://jsonplaceholder.typicode.com/posts/1")
- .then((response)=>response.json())
- .then((json) => console.log(json))
Promise的鏈式調(diào)用避免了代碼層層嵌套,盡管有很長的鏈式調(diào)用,但也只是將代碼向下方增長而不是向右。可讀性會大大提高。
但是在使用異步操作的時候也會遇到錯誤,比如各種網(wǎng)絡(luò)問題以及數(shù)據(jù)格式不正確等。然后我們可以通過在末尾添加一個catch()來捕獲這些錯誤,如果之前任意一個階段發(fā)生了錯誤,那么catch會被觸發(fā),然后之后的then將不會再執(zhí)行
這跟同步編程中用到的try/catch塊相似,Promise還提供了finally方法,會在Promise鏈結(jié)束后調(diào)用,無論是否出現(xiàn)錯誤,我們都可以在這里做函數(shù)清理的工作。畢竟要有首有尾嘛。
async/await
現(xiàn)在來看一下async/await,簡單來說就是基于Promise之上的語法糖,可以讓異步操作更加簡單明了。
具體步驟就是
首先使用async將返回值為Promise對象的函數(shù)標記為異步函數(shù),就像是剛剛用到的fetch()就是一個異步函數(shù),在異步函數(shù)中可以調(diào)用其他異步函數(shù),不過不是使用then,而是用更簡單的await,中文意思就是等到,等待,所以await會等待Promise完成之后直接返回最終結(jié)果
用了async/await之后就是將函數(shù)變成異步函數(shù),可以直接獲取到我們想要的結(jié)果,所以some已經(jīng)是服務(wù)器返回的響應(yīng)數(shù)據(jù)了,然后我們就可以進行響應(yīng)的操作了
await雖然看上去會暫停函數(shù)的執(zhí)行,但是在等待的過程中同樣可以處理其他任務(wù),比如我這里將返回的數(shù)據(jù)轉(zhuǎn)換為json數(shù)據(jù),因為await底層就是基于Promise和事件循環(huán)機制實現(xiàn)的,具體操作還有很多自行去嘗試。
這樣我們就拿到了我們想要的數(shù)據(jù)了。
但是使用的時候也要留意await的錯誤用法
比如我這樣
- async function fn() {
- const some1 = await fetch("http://jsonplaceholder.typicode.com/posts/1")
- const some2 = await fetch("http://jsonplaceholder.typicode.com/posts/2")
- const some3 = await fetch("http://jsonplaceholder.typicode.com/posts/3")
- ...
- }
- fn()
雖然看起來沒有什么錯誤啊,但是這樣寫會打破這兩個fetch()操作的并行,因為我們是等到第一個任務(wù)完成再執(zhí)行第二個任務(wù),然后再執(zhí)行后面的代碼。
所以我們有個小妙招。
就是將所有Promise用Promise.all組合起來,然后再去await,比如我下面的做法
- async function fn() {
- const some1 = await fetch("http://jsonplaceholder.typicode.com/posts/1")
- const some2 = await fetch("http://jsonplaceholder.typicode.com/posts/1")
- const some3 = await fetch("http://jsonplaceholder.typicode.com/posts/1")
- const [a,b,c] = await Promise.all([some1,some2,some3])
- }
- fn()
這樣的做法會讓運行程序效率提升很多。
最后,我們不能在全局或者普通函數(shù)中直接使用await關(guān)鍵字,await只在異步中有效,如果我們想要在最外層中使用await那么需要先定義一個異步函數(shù),然后再在函數(shù)體中使用它
使用async await可以寫更清晰更容易的理解異步代碼,而且不用再使用底層的Promise對象,包括then(),catch()函數(shù)等
如果舊版本瀏覽器不支持async await語法,可以通過轉(zhuǎn)譯器編譯成舊版本也兼容的代碼