ES6—Async與異步編程(11)

單線程是Javascript語言最本質的特性之一,Javascript引擎在運行js代碼的時候,同一個時間只能執行單個任務。jquery

這種模式的好處是實現起來比較簡單,執行環境相對單純。編程

壞處是隻要有一個任務耗時很長,後面的任務都必須排隊等着,會拖延整個程序的執行。常見的瀏覽器無響應(假死),每每就是由於某一段Javascript代碼長時間運行(好比死循環),致使整個頁面卡在這個地方,其餘任務沒法執行。瀏覽器

因此異步編程對JavaScript語言過重要。異步

有些小夥伴可能還不太理解"異步"。async

所謂的"異步",就是一個任務分紅兩段,先執行第一段,而後轉而執行其餘任務,等作好了準備,再回過頭執行第二段。ide

例如,有一個任務是讀取文件進行處理,任務的第一段是向操做系統發出請求,要求讀取文件。而後,程序執行其餘任務,等到操做系統返回文件,再接着執行任務的第二段(處理文件)。這種不連續的執行,就叫作異步。異步編程

相應地,連續的執行就叫作同步。因爲是連續執行,不能插入其餘任務,因此操做系統從硬盤讀取文件的這段時間,程序只能乾等着。函數

講的通俗點:性能

朱自清的《背影》中,父親對朱自清說 :「我買幾個橘子去。你就在此地,不要走動。」url

朱自清沒有走動,等着買完橘子的父親一塊兒吃橘子,就叫同步。

若是朱自清沒有等父親,獨自走了,那就不能和父親一塊兒吃橘子,就叫異步。

一、異步編程

咱們就以用戶註冊這個特別常見的場景爲例,講講異步編程。

第一步,驗證用戶是否註冊

第二步,沒有註冊,發送驗證碼

第三步,填寫驗證碼、密碼,檢驗驗證碼是否正確

這個過程是有必定的順序的,你必須保證上一步完成,才能順利進行下一步。

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聲明便可。若是忽略這些額外的關鍵字,簡直就是完徹底全的同步寫法。

二、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

三、並聯中的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)));
}

多個異步操做,若是沒有繼承關係,最好同時觸發。

四、總結

從最先的回調函數,到 Promise 對象,每次都有所改進,但又讓人以爲不完全。它們都有額外的複雜性,都須要理解抽象的底層運行機制。

例若有三個請求須要發生,第三個請求是依賴於第二個請求的結果,第二個請求依賴於第一個請求的結果。若用 ES5實現會有3層的回調,致使代碼的橫向發展。若用Promise 實現至少須要3個then,致使代碼的縱向發展。然而,async/await 解決了這些問題。

從實現上來看 async/await 是在 生成器、Promise 基礎上構建出來的新語法:以生成器實現流程控制,以 Promise 實現異步控制。

可是,不要所以小看 async/await,使用同步的方式寫異步代碼其實很是強大。

async/await 在語義化、簡化代碼、錯誤處理等方面有不少的優點,畢竟用async/ wait編寫條件代碼要簡單得多,還可使用相同的代碼結構(衆所周知的try/catch語句)處理同步和異步錯誤,因此常被稱爲JavaScript異步編程的終極解決方案,可見其重要性和優點。

但願小夥們在之後的實戰項目中,多多練習,才能掌握async/await的真正精要。

相關文章
相關標籤/搜索