ES6—Async與非同步程式設計(11)
單執行緒是Javascript語言最本質的特性之一,Javascript引擎在執行js程式碼的時候,同一個時間只能執行單個任務。
這種模式的好處是實現起來比較簡單,執行環境相對單純。
壞處是隻要有一個任務耗時很長,後面的任務都必須排隊等著,會拖延整個程式的執行。常見的瀏覽器無響應(假死),往往就是因為某一段Javascript程式碼長時間執行(比如死迴圈),導致整個頁面卡在這個地方,其他任務無法執行。
所以非同步程式設計對JavaScript語言太重要。
有些小夥伴可能還不太理解"非同步"。
所謂的"非同步",就是一個任務分成兩段,先執行第一段,然後轉而執行其他任務,等做好了準備,再回過頭執行第二段。
例如,有一個任務是讀取檔案進行處理,任務的第一段是向作業系統發出請求,要求讀取檔案。然後,程式執行其他任務,等到作業系統返回檔案,再接著執行任務的第二段(處理檔案)。這種不連續的執行,就叫做非同步。
相應地,連續的執行就叫做同步。由於是連續執行,不能插入其他任務,所以作業系統從硬碟讀取檔案的這段時間,程式只能乾等著。
講的通俗點:
朱自清的《背影》中,父親對朱自清說 :“我買幾個橘子去。你就在此地,不要走動。”
朱自清沒有走動,等著買完橘子的父親一起吃橘子,就叫同步。
如果朱自清沒有等父親,獨自走了,那就不能和父親一起吃橘子,就叫非同步。
1、非同步程式設計
我們就以使用者註冊這個特別常見的場景為例,講講非同步程式設計。
第一步,驗證使用者是否註冊
第二步,沒有註冊,傳送驗證碼
第三步,填寫驗證碼、密碼,檢驗驗證碼是否正確
這個過程是有一定的順序的,你必須保證上一步完成,才能順利進行下一步。
1.1 回撥函式
function testRegister(){}// 驗證使用者是否註冊 function sendMessage(){}// 給手機發送驗證碼x function testMessage(){}// 檢驗驗證碼是否正確 function doRegister(){//開始註冊 testRegister(data){ if(data===false){ //已註冊 }else{ //未註冊 sendMessage(data){ if(data===true){ //傳送驗證碼成功 testMessage(data){ if(data===true){//驗證碼正確 }else{//驗證碼不正確 } } } } } } }
程式碼中就已經有許多問題,比如雜亂的 if 判斷語句 、層層巢狀的函式,造成程式碼的可讀性差,難於維護。
另外,如果在層層回撥函式中出現異常,除錯起來是非常讓人奔潰的 —— 由於 try-catch 無法捕獲非同步的異常,我們只能不斷不斷的寫 debugger 去追蹤,簡直步步驚心。
這種層層巢狀被稱為回撥地獄。
1.2 Promise方式
Promise就是為了解決回撥地獄問題而提出的。它不是新的語法功能,而是一種新的寫法,允許將回調函式的巢狀,改成鏈式呼叫。採用Promise,連續讀取多個檔案,寫法如下。
let state=1;//模擬返回結果 function step1(resolve,reject){ console.log('1. 驗證使用者是否註冊'); if(state==1){ resolve('未註冊'); }else{ reject('已註冊'); } } function step2(resolve,reject){ console.log('2.給手機發送驗證碼'); if(state==1){ resolve('傳送成功'); }else{ reject('傳送失敗'); } } function step3(resolve,reject){ console.log('3.檢驗驗證碼是否正確'); if(state==1){ resolve('驗證碼正確'); }else{ reject('驗證碼不正確'); } } new Promise(testRegister).then(function(val){ // 驗證使用者是否註冊 console.log(val); return new Promise(sendMessage);// 給手機發送驗證碼 }).then(function(val){ console.log(val); return new Promise(testMessage);// 檢驗驗證碼是否正確 }).then(function(val){ console.log(val); return val; });
回撥函式採用了巢狀的方式依次呼叫testRegister()、sendMessage() 和testMessage(),而Promise使用then將它們連結起來。
相比回撥函式而言,Promise程式碼可讀性更高,程式碼的執行順序一目瞭然。
Promise的方式雖然解決了回撥地獄,但是最大問題是程式碼冗餘,原來的任務被Promise 包裝了一下,不管什麼操作,一眼看去都是一堆 then,原來的語義變得很不清楚。程式碼流程不能很好的表示執行流程。
大家初中學過電路,這個就像電路的串聯,如果沒學過也沒關係,你肯定知道jquery有鏈式操作,這個就很類似鏈式操作的寫法,比較符合我們的思維邏輯。
1.3 async/await方式
async語法是對new Promise的包裝,await語法是對then方法的提煉。
async function doRegister(url) { let data= await testRegister();// 驗證使用者是否註冊 let data2 = await sendMessage(data);// 給手機發送驗證碼 let data3 = await testMessage(data2); // 檢驗驗證碼是否正確 return data3 }
上面的程式碼雖然短,但是每一句都極為重要。data 是 await testRegister的返回結果,data2 又使用了 data 作為sendMessage的引數,data3 又使用了data2 作為testMessage的引數。
只要在doRegister前面加上關鍵詞async,在函式內的非同步任務前新增await宣告即可。如果忽略這些額外的關鍵字,簡直就是完完全全的同步寫法。
2、async用法
2.1 返回 Promise 物件
async函式返回一個 Promise 物件。
async函式內部return語句返回的值,會成為then方法回撥函式的引數。
async function f() { return 'aaa'; } f().then(v => console.log(v)) //aaa //Promise {<resolved>: undefined}
2.2 await 命令
正常情況下,await命令後面是一個 Promise 物件,返回該物件的結果。如果不是 Promise 物件,就直接返回對應的值。
/*成功情況*/ async function f() { return await 123; } f().then(value => console.log(value));// 123 /*失敗情況*/ async function f() { return Promise.reject('error'); } f().catch(e => console.error(e));// error
注意事項:
await命令只能用在async函式之中,如果用在普通函式,就會報錯。
/* 錯誤處理 */ function f(db) { let docs = [1, 2, 3]; for(let doc of docs) { await db.push(doc); } return db; // Uncaught SyntaxError: Unexpected identifier } /* 正確處理(順序執行) */ async function f(db) { let docs = [1, 2, 3]; for(let doc of docs) { await db.push(doc); } return db; }
2.3 async中異常處理
通過使用 async/await,我們就可以配合 try/catch 來捕獲非同步操作過程中的問題,包括 Promise 中reject 的資料。
await後面可能存在reject,需要進行try…catch程式碼塊中
async function f() { try { await Promise.reject('出錯了'); } catch(e) { console.error(e); } return Promise.resolve('hello'); } f().then(v => console.log(v));// 出錯了 hello
3、並聯中的await
async/await 語法確實很簡單好用,但也容易使用不當,還要根據具體的業務場景需求來定。
例如我們需要獲取一批圖片的大小資訊:
async function allPicInfo (imgs) { const result = []; for (const img of imgs) { result.push(await getSize(img)); } }
程式碼中的每次 getSize 呼叫都需要等待上一次呼叫完成,同樣是一種效能浪費,而且花費的時間也長。同樣的功能,用這樣的方式會更合適:
async function allPicInfo (imgs) { return Promise.all(imgs.map(img => getSize(img))); }
多個非同步操作,如果沒有繼承關係,最好同時觸發。
4、總結
從最早的回撥函式,到 Promise 物件,每次都有所改進,但又讓人覺得不徹底。它們都有額外的複雜性,都需要理解抽象的底層執行機制。
例如有三個請求需要發生,第三個請求是依賴於第二個請求的結果,第二個請求依賴於第一個請求的結果。若用 ES5實現會有3層的回撥,導致程式碼的橫向發展。若用Promise 實現至少需要3個then,導致程式碼的縱向發展。然而,async/await 解決了這些問題。
從實現上來看 async/await 是在 生成器、Promise 基礎上構建出來的新語法:以生成器實現流程控制,以 Promise 實現非同步控制。
但是,不要因此小看 async/await,使用同步的方式寫非同步程式碼其實非常強大。
async/await 在語義化、簡化程式碼、錯誤處理等方面有很多的優勢,畢竟用async/ wait編寫條件程式碼要簡單得多,還可以使用相同的程式碼結構(眾所周知的try/catch語句)處理同步和非同步錯誤,所以常被稱為JavaScript非同步程式設計的終極解決方案,可見其重要性和優勢。
希望小夥們在以後的實戰專案中,多多練習,才能掌握async/await的真正精要。