還在找什么,JavaScript的異步編程解決方案全在這里了
阿里巴巴前端工程師逸翾對JavaScript中的異步編程進行了詳細講解。JavaScript的特點就是單線程,本文首先對單線程異步的原理進行了解讀,接著著重分析了JavaScript異步解決方案,詳述了Callback、Promise、Generator、Async/Await的特性和使用原理。
以下是精彩視頻內容整理:
單線程異步
JavaScript語言的一大特點就是單線程,在某個特定的時刻只有特定的代碼能夠被執(zhí)行,并阻塞其它的代碼,也就是說,同一個時間只能做一件事。
常用的異步操作最主要有網絡請求,請求會有響應時間,在響應結果回來之前要處理其它事情;IO操作,比如讀取某個文件,在此過程中做其它事情;定時函數(shù)。
異步是由瀏覽器的兩個或以上常駐線程共同完成,列如異步請求:JS執(zhí)行線程發(fā)起異步請求,瀏覽器開一條新的http請求來執(zhí)行請求,當監(jiān)視到請求已完成,它會把函數(shù)插入到JS執(zhí)行隊列的尾部等待處理。
通過如圖所示定時函數(shù)實例,可以看出異步處理過程。先是打印一個1,然后執(zhí)行setTimeout函數(shù),約定在0秒后再打印一個2,然后打印一個3,執(zhí)行結果為先打印1,再打印3,***再打印2,說明整個過程已經發(fā)生異步過程。
Javascript有一個主要的執(zhí)行主線程,但是setTimeout、IO操作、網絡異步請求等都會有一個回調函數(shù),這些操作都是通過瀏覽器API實現(xiàn)的,當定時器時間到了或者請求結果回來時,會將回調函數(shù)push到一個javascript任務隊列中,當javascript主線程任務執(zhí)行完畢后,才會從任務隊列中繼續(xù)拿等待代碼再執(zhí)行。具體流程總結如下:
所有同步任務都在主線程上執(zhí)行,形成一個執(zhí)行棧(execution context stack)。
主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
一旦"執(zhí)行棧"中的所有同步任務執(zhí)行完畢,系統(tǒng)就會讀取"任務隊列",看看里面有哪些事件。那些對應的異步任務,于是結束等待狀態(tài),進入執(zhí)行棧,開始執(zhí)行。
主線程不斷重復上面的第三步。
異步解決方案
了解了Javascript單線程與異步關系,如何書寫日常工作中的工程代碼呢?怎樣能在異步請求結束后執(zhí)行特定方法?目前,javascript異步編程經歷了幾個階段,且在不斷演進。
Callback
最開始是回調函數(shù),當事件完成后執(zhí)行回調函數(shù),相當于完成異步操作。我們可以像使用變量一樣使用函數(shù),作為另一個函數(shù)的參數(shù),在另一個函數(shù)中作為返回結果,在另一個函數(shù)中調用它。當我們作為參數(shù)傳遞一個回調函數(shù)給另一個函數(shù)時,我們只傳遞了這個函數(shù)的定義,并沒有在參數(shù)中執(zhí)行它。當函數(shù)擁有了在參數(shù)中定義的回調函數(shù)后,它可以在任何時候調用(也就是回調)它。
我們在工作中最長接觸的就是發(fā)送Ajax異步請求,在***個請求發(fā)送完后,用它的參數(shù)處理第二個請求,再用第二個請求的參數(shù)請求第三個,三層嵌套代碼如圖所示。
如圖所示為mongoDB在node.js中的實例,嵌套了六層。當多個異步事務多級依賴時,回調函數(shù)會形成多級的嵌套,代碼變成金字塔型結構。雖然能解決異步問題,但這使得代碼得看難懂,更使得調試、重構的過程充滿風險。
Promise
Promise比傳統(tǒng)解決方案更加合理和強大,是更加好的異步解決方案,promise對象有以下四個特點:
- promise 可能有三種狀態(tài):等待(pending)、已完成(resolved)、已拒絕(rejected)。
- promise 的狀態(tài)只可能從“等待”轉到“完成”態(tài)或者“拒絕”態(tài),不能逆向轉換,同時“完成”態(tài)和“拒絕”態(tài)不能相互轉換。
- promise對象必須實現(xiàn)then方法,而且then必須返回一個promise ,同一個 promise 的then可以調用多次(鏈式),并且回調的執(zhí)行順序跟它們被定義時的順序一致。
- then方法接受兩個參數(shù),***個參數(shù)是成功時的回調,在 promise 由“等待”態(tài)轉換到“完成”態(tài)時調用,另一個是失敗時的回調,在 promise 由“等待”態(tài)轉換到“拒絕”態(tài)時調用
如圖所示,左側創(chuàng)建promise對象,隔2000秒后執(zhí)行,***返回promise對象。Promise有立即執(zhí)行性,當創(chuàng)建完對象后,在已完成或已拒絕之前代碼都會執(zhí)行一遍。右側為打印結果,可以看到創(chuàng)建promise對象被打印出來,并成功生成promise對象,當執(zhí)行then時,此時已經返回成功狀態(tài),所以一直執(zhí)行成功的回調函數(shù)。
那么,promise如何解決異步問題的呢?又有哪些特點呢?具體如下:
代碼看起來更加符合邏輯、可讀性更強。
Promise并沒有改變JS異步執(zhí)行的本質,從寫法上甚至還能看到一點點callback的影子。
如圖所示為鏈式調用方式,***個請求成功后,就去執(zhí)行下一個,如果任何一個請求未成功,直接catch掉error,并打印出來。
Generator
我們希望以同步方式寫異步代碼,可以使邏輯更加清晰,代碼量減少更多。為了實現(xiàn)這種目標,我們又演進出generator解決方案。
Generator函數(shù)很特殊,理解起來比promise和callback更加難,從語法上將,generator具備以下特質:
- 定義Generator時,需要使用function*。
- 使用時生成一個Generator對象。
- 執(zhí)行.next()激活暫停狀態(tài),開始執(zhí)行內部代碼,直到遇到y(tǒng)ield,返回此時執(zhí)行的結果,并記住此時執(zhí)行的上下文,暫停。
- 再次執(zhí)行.next()時重復第三步。
如圖所示,首先定義了test=add(5),函數(shù)并沒有執(zhí)行,只是生成了一個generator對象,函數(shù)屬于暫停狀態(tài),只有當執(zhí)行.next()時,才會激活暫停狀態(tài),開始執(zhí)行內部代碼,直到遇到***個yield才會返回執(zhí)行結果,并記住上下文,暫停,交出控制權。再次執(zhí)行.next()時,找到第二個yield,再次記住上下文,暫停,交出控制權,依此重復。
如圖所示,封裝一個異步任務,定義一個generator對象,執(zhí)行請求后交出控制權,通過promise判斷是否要繼續(xù)執(zhí)行。在異步事件發(fā)生后,首先將控制權交給別人,讓程序執(zhí)行其它代碼,當異步事件完成后,將控制權搶回來,繼續(xù)執(zhí)行其它操作。Generator需要一個自動執(zhí)行器配合使用,實現(xiàn)正常思維下的異步處理,有了自動執(zhí)行器,異步請求可以用同步的方式寫異步代碼,直接將請求全部寫在一起。
通過進一步的演進,async和await函數(shù)應運而生。它們的個性特征如下:
- async 表示這是一個async函數(shù),而await只能在這個函數(shù)里面使用。
- await 表示在這里等待await后面的操作執(zhí)行完畢,再執(zhí)行下一句代碼。
- await 后面緊跟著的***是一個耗時的操作或者是一個異步操作。
- await后面必須是一個Promise對象,如果不是會被轉化為一個已完成狀態(tài)的Promise
Async與generator的寫法類似,本質上是generator的語法糖。其內置執(zhí)行器,具備更好的語義,更廣的適用性。并且返回值是Promise。