亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

ES6+ async/await

1. 前言

前面幾節我們已經學習了解決異步的方案 Promise 和 Generator,上一節我們也通過一個案例使用 Promise + Generator 實現了一個比較好的異步解決方案。同時我們實現了一個簡版的 co 庫,讓我們在使用 Generator 函數處理異步任務時更加方便,但這不是最完美的解決方案。

本節我們將學習 ES7 推出的 async/await 其特性是對 JS 的異步編程進行了重要的改進,在不阻塞主線程的情況下,它給我們提供了使用同步代碼的風格來編寫異步任務的能力。另外,我們要明確的是 async/await 其實是 Promise + Generator 的語法糖,為了幫助我們像寫同步代碼一樣書寫異步代碼,代碼風格更優雅,錯誤捕獲也更容易。

本節我們將通過對上一節案例的改造。在不需要 co 庫的情況下直接使用 async/await 讓我們更加深刻地理解異步方案的演變過程。

2. 改造上節案例

上一節 我們通過一個案例來講解 Promise + Generator 在實際應用中的使用,通過 Generator 函數 和 yield 讓異步代碼看起來像同步代碼一樣執行。但是這樣里面存在的一個問題就是生成器函數直接執行,需要手動處理。為了解決深層回調的問題我們借助了 co 庫來幫助我們去執行生成器函數,從而解決了回調地獄的問題。下面是上一節的代碼。

const ajax = function(api) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (api === 'api_1') {
        resolve('api_2');
      }
      if (api === 'api_2') {
        resolve(100);
      }
    }, 0)
  })
}

function * getValue() {
  const api = yield ajax('api_1');
  const value = yield ajax(api);
  return value;
}

co(getValue()).then(res => {
  console.log(res);
})

上面的代碼中 getValue 是生成器函數,不能直接調用,這里用 co 庫來進行執行,然后通過 Promise 的鏈式調用獲取執行后的結果。但是這里借助了 co 的庫,我們其實最希望的是能像執行普通函數一樣直接調用 getValue 就能執行并得到結果。 async/await 的出現就是為了抹平在調用時所做的額外步驟。那讓我們看看 async/await 是怎么用的:

async function getValue() {
  const api = await ajax('api_1');
  const value = await ajax(api);
  console.log(value)
  return value;
}
getValue()	// 控制臺打印 value的值是:100

上面的代碼中我們可以看出使用 async/await 定義的 getValue 函數和生成器函數 */yield 定義的基本相同,但是在執行時 async/await 定義的函數直接調用即可。從這里我們就能看到 async/await 的優點,無需過多的操作非常優雅和簡潔。

3. 用法

上面我們基本了解了 async 函數,下面我們就來看看它的基本使用和需要注意的地方。

定義一個異步函數時需要使用 asyncfunction 關鍵字一起來完成,類似生成器函數中的 yield 來暫停異步任務,在 async 函數中使用 await 關鍵去等待異步任務返回的結果。

async 函數其本質是 Promise + Generator 函數組成的語法糖,它為了減少了 Promise 的鏈式調用,解放了 Generator 函數的單步執行。主要語法如下:

async function name([param[, param[, ... param]]]) { 
    statements 
}

上面代碼中的 statements 是函數主體的表達式,async 函數可以通過 return 來返回一個值,這個返回值會被包裝成一個 Promise 實例,可以被鏈式調用。下面我們來看兩段等價代碼。

// 下面兩段代碼時相同的
async function foo() {
   return 100
}
function foo() {
   return Promise.resolve(100)
}

// 下面兩段代碼時相同的
async function foo() {
  await 1;
}
function foo() {
   return Promise.resolve(1).then(() => undefined)
}

上面的兩段代碼效果時相同的,這里我們就不去探究 async 函數是怎么實現的,其大概原理類似上節寫的 co 庫,有興趣的小伙伴可以去 babel 上去看看 async 函數編譯是什么樣子的。

當在 async 函數中返回的是一個普通值或 await 后跟一個普通值時,此時的 async 函數是同步的。在 Promise 中失敗是不能被 try...catch 捕獲的,需要通過 catch 的方式來捕獲錯誤。而使用 async 函數則是可以通過 try...catch 來捕獲。

async function foo() {
	return new Error('Throw an error');
}

foo().then(res => {
  console.log(res)
}).catch(err => {
  console.error(err)	// Error: Throw an error
})

async function foo2() {
  try{
    var v = await foo()
    console.log(v)
  } catch(e) {
    console.log(e);	// Error: Throw an error
  }
}
foo2()

上面的代碼中在執行 foo() 直接拋出了一個錯誤,而 Promise 和 async/await 對錯誤的捕獲是不同的,我們知道 Promise 是通過 then 中的失敗回調和 catch來捕獲錯誤的,而 async 函數使用的是 try...catch 更像同步的方式。

3.1 錯誤捕獲

但是有個問題,當程序需要同時處理多個異步任務時,那我們使用 async/await 怎樣捕獲那個異步任務出現錯誤呢?try 塊中的代碼只要程序出現錯誤就會拋出錯誤,但是不知道是哪個異步任務出錯了不利于定位問題。如果使用多個 try...catch :

const task = function (num) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (num === 300) {
        reject('throw error')
      } else {
       	resolve('imooc'); 
      }
    }, 1000)
  })
}

async function foo() {
  try {
    let res1 = await task(100);
    try {
      let res2 = await task(200);
      try {
        let res3 = await task(300);
      } catch(e) {
        console.log('res3', e)
      }
    } catch(e) {
      console.log('res2', e)
    }
  } catch(e) {
    console.log('res1', e)
  }
}
foo()	// res3 throw error

看到上面的代碼你是不是覺得很難受啊,又回到了嵌套地獄的原始問題了。async 函數在異常捕獲時,沒有非常完美的解決方案,這主要源自依賴 try...catch 對錯誤的捕獲。但有一些還算比較優雅的解決方案,我們已經知道了 async 函數返回的是一個 Promise 那么我們是不是可以使用 Promise 的 catch 來捕獲呢?答案是當然的呢。

async function foo() {
  let res1 = await task(100).catch(err => console.log('res1', err));
  let res2 = await task(200).catch(err => console.log('res2', err));
  let res3 = await task(300).catch(err => console.log('res3', err));
}
foo()	// res3 throw error

上面的代碼看起來就比嵌套的 try...catch 感覺好很多,這也是一個比較好的解決方式。在使用 catch 時需要弄清楚 Promise 和 async 函數之間的關系,不然就很難理解這種寫法。

3.2 濫用 async/await

既然 async/await 這么優雅簡潔,那在編程的過程中都使用這個就好啦!其實這里是一個坑,很多時候 async/await 都會被濫用導致程序卡頓,執行時間過長。

async function foo() {
  let res1 = await task(100);
  let res2 = await task(200);
  let res3 = await task(300);

  return { res1, res2, res3 }
}
foo()

在很多時候我們會寫成這樣的代碼,如果后一個任務依賴前一個任務這樣寫完全沒問題,但是如果是三個獨立的異步任務,那這樣寫就會導致程序執行時間加長。這樣的代碼過于同步化,我們需要牢記的是 await 看起來是同步的,但它仍然屬于異步的內容,最終還是走的回調,只是語言底層給我們做了很多工作。

針對沒有關聯的異步任務我們需要把它們解開,

const task = function (num) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
       	resolve('imooc ' + num); 
    }, 1000)
  })
}

async function foo() {
  let res1Promes = task(100);
  let res2Promes = task(200);
  let res3Promes = task(300);

  let res1 = await res1Promes;
  let res2 = await res2Promes;
  let res3 = await res3Promes;

	console.log({ res1, res2, res3 })

  return { res1, res2, res3 }
}
foo();	// { res1: 'imooc 100', res2: 'imooc 200', res3: 'imooc 300' }

這里需要明白的一點是:為什么要把 task 拿到 await 外面去執行呢?await 的本質就是暫停異步任務,等待返回結果,等到結果返回后就會繼續往下執行。還要知道的是每個 task 都是一個異步任務,像之前的那種寫法,await 會等待上一個異步任務完成才會走下一個。而我們把 task 拿出來了,也就是每個 task 會按照異步的方式去執行。這個時候三個 task 都已經開始執行了,當遇到 await 就只需要等到任務完成就行。所需要的時間是異步任務中耗時最長的,而不是之前的總和。

4. 小結

本節我們主要通過延續上一節的案例,用 async 函數給出了最優的解決方案,從而完善了整個異步演變的過程,讓我們更加清晰地理解為什么會有 Promise?為什么會有生成器?為什么會有 async/await?由淺入深層層遞進地講解了 ES6 以后對異步任務處理的演變。然后我們主要學習了 async 函數的基本使用和錯誤處理的捕獲。最后,我們講解了如果不濫用 async 函數的案例,讓我們在以后寫程序的過程中更加得心應手。