JavaScript基礎——深入學習async/await

騰訊雲加社群發表於2018-12-20

本文由雲+社群發表

本篇文章,小編將和大家一起學習非同步程式設計的未來——async/await,它會打破你對上篇文章Promise的認知,竟然非同步程式碼還能這麼寫! 但是別太得意,你需要深入理解Promise後,才能更好的的駕馭async/await,因為async/await是基於Promise的。

關於async / await

  1. 用於編寫非同步程式
  2. 程式碼書寫方式和同步編碼十分相似,因此程式碼十分簡潔易讀
  3. 基於Promise
  4. 您可以使用try-catch常規的方法捕獲異常
  5. ES8中引入了async/await,目前幾乎所有的現代瀏覽器都已支援這個特性(除了IE和Opera不支援)
  6. 你可以輕鬆設定斷點,除錯更容易。

從async開始學起

讓我們從async關鍵字開始吧,這個關鍵字可以放在函式之前,如下所示:

async function f() { 
 return 1; 
}
複製程式碼

在函式之間加上async意味著:函式將返回一個Promise,雖然你的程式碼裡沒有顯示的宣告返回一個Promise,但是編譯器會自動將其轉換成一個Promise,不信你可以使用Promisethen方法試試:

async function f() { 
 return 1; 
} 
f().then(alert); // 1
複製程式碼

...如果你不放心的話,你可以在程式碼裡明確返回一個Promise,輸出結果是相同的。

async function f() { 
 return Promise.resolve(1); 
} 
f().then(alert); // 1
複製程式碼

很簡單吧,小編之所以說 async/await 是基於Promise是沒毛病的,async函式返回一個Promise,很簡單吧,不僅如此,還有一個關鍵字await,await只能在async中執行。

等待——await

await的基本語法:

let value=await promise;
複製程式碼

該關鍵字的await的意思就是讓JS編譯器等待Promise並返回結果。接下來我們看一段簡單的示例:

async function f() { 
 let promise = new Promise((resolve, reject) => { 
 setTimeout(() => resolve("done!"), 1000) 
 }); 
 let result = await promise; // wait till the promise resolves (*) 
 alert(result); // "done!" 
} 
f();
複製程式碼

函式執行將會在 let result = await promise 這一行暫停,直到Promise返回結果,因此上述程式碼將會1秒後,在瀏覽器彈出“done”的提示框。

小編在此強調下:

  • await的字面意思就是讓JavaScript等到Promise結束,然後輸出結果。這裡並不會佔用CPU資源,因為引擎可以同時執行其他任務:其他指令碼或處理事件。
  • 不能單獨使用await,必須在async函式作用域下使用,否則將會報出異常“Error: await is only valid in async function”,示例程式碼如下:
function f() { 
 let promise = Promise.resolve(1); 
 let result = await promise; // Syntax error 
}
複製程式碼

接下來,小編將和大家一起來親自動手實踐以下內容:

  • async與Promise.then的結合,依次處理多個結果
  • 使用await替代Promise.then,依次處理多個結果
  • 同時等待多個結果
  • 使用Promise.all收集多個結果
  • 使用try-catch捕獲異常
  • 如何捕獲Promise.all中的異常
  • 使用finally確保函式執行

一起動手之前,確保你安裝了Node,NPM相關工具,谷歌瀏覽器,為了預覽程式碼效果,小編使用 npm install http-server -g 命令快速部署了web服務環境,方便我們執行程式碼。接下來,我們寫一個火箭發射場景的小例子(不是真的發射火箭?)。

async與Promise.then的結合,依次處理多個結果

  • 通過控制檯命令切換至工作區
  • 建立一個async-function-Promise-chain的資料夾
  • 在main.js中用建立一個返回隨機函式的async函式getRandomNumber:
async function getRandomNumber() { 
 console.log('Getting random number.'); 
 return Math.random(); 
} 
複製程式碼
  • 再建立一個async函式determinReadyToLaunch:如果傳入引數大於0.5將返回True
async function deteremineReadyToLaunch(percentage) { 
 console.log('Determining Ready to launch.'); 
 return percentage>0.5; 
} 
複製程式碼
  • 建立第三個async函式reportResults,如果傳入引數為True將進入倒數計時發射
async function reportResults(isReadyToLaunch) { 
 if (isReadyToLaunch) { 
 console.log('Rocket ready to launch. Initiate countdown: ?'); 
 } else { 
 console.error('Rocket not ready. Abort mission: '); 
 } 
} 
複製程式碼
  • 建立一個main函式,呼叫getRandomNumber函式,並且通過Promise.then方法相繼呼叫determineReadyToLaunch和reportResults函式
export function main() { 
 console.log('Before Promise created'); 
 getRandomNumber() 
 .then(deteremineReadyToLaunch) 
 .then(reportResults) 
 console.log('After Promise created'); 
} 
複製程式碼
  • 新建一個html檔案引入main.js
<html> 
<script type="module"> 
 import {main} from './main.js'; 
 main(); 
</script> 
<body> 
</body> 
</html>
複製程式碼
  • 在工作區域執行 http-server 命令,你將會看到如下輸出

img

使用await替代Promise.then,依次處理多個結果

上一小節,我們使用Promise.then依次處理了多個結果,本小節,小編將使用await實現同樣的功能,具體操作如下:

  • 通過控制檯命令切換至工作區
  • 建立一個async-function-Promise-chain的資料夾
  • 在main.js中用建立一個返回隨機函式的async函式getRandomNumber:
async function getRandomNumber() { 
 console.log('Getting random number.'); 
 return Math.random(); 
} 
複製程式碼
  • 再建立一個async函式determinReadyToLaunch:如果傳入引數大於0.5將返回True
async function deteremineReadyToLaunch(percentage) { 
 console.log('Determining Ready to launch.'); 
 return percentage>0.5; 
} 
複製程式碼
  • 建立第三個async函式reportResults,如果傳入引數為True將進入倒數計時發射
async function reportResults(isReadyToLaunch) { 
 if (isReadyToLaunch) { 
 console.log('Rocket ready to launch. Initiate countdown: ?'); 
 } else { 
 console.error('Rocket not ready. Abort mission: '); 
 } 
} 
複製程式碼
  • 建立一個main函式,呼叫getRandomNumber函式,並且通過Promise.then方法相繼呼叫determineReadyToLaunch和reportResults函式
export async function main() { 
 const randomNumber = await getRandomNumber(); 
 const ready = await deteremineReadyToLaunch(randomNumber); 
 await reportResults(ready); 
} 
複製程式碼
  • 在工作區域執行 http-server 命令,你將會看到如下輸出

img

同時等待多個結果

有時候我們需要同時啟動多個非同步,無需依次等待結果消耗時間,接下來的例子可以使用await同時啟動多個非同步函式,等待多個結果。

  • 通過控制檯命令切換至工作區
  • 建立一個await-concurrently的資料夾
  • 建立三個函式checkEngines,checkFlightPlan,和checkNavigationSystem用來記錄資訊時,這三個函式都返回一個Promise,示例程式碼如下:
function checkEngines() { 
 console.log('checking engine'); 
 return new Promise(function (resolve) { 
 setTimeout(function() { 
  console.log('engine check completed'); 
  resolve(Math.random() < 0.9) 
 }, 250) 
 }); 
} 
function checkFlightPlan() { 
 console.log('checking flight plan'); 
 return new Promise(function (resolve) { 
 setTimeout(function() { 
  console.log('flight plan check completed'); 
  resolve(Math.random() < 0.9) 
 }, 350) 
 }); 
} 
function checkNavigationSystem() { 
 console.log('checking navigation system'); 
 return new Promise(function (resolve) { 
 setTimeout(function() { 
  console.log('navigation system check completed'); 
  resolve(Math.random() < 0.9) 
 }, 450) 
 }); 
} 
複製程式碼
  • 建立一個async的main函式呼叫上一步建立函式。將每個返回的值分配給區域性變數。然後等待Promise的結果,並輸出結果:
 export async function main() { 
 const enginePromise = checkEngines(); 
 const flighPlanPromise = checkFlightPlan(); 
 const navSystemPromise = checkNavigationSystem(); 
 const enginesOk = await enginePromise; 
 const flighPlanOk = await flighPlanPromise; 
 const navigationOk = await navSystemPromise; 
 if (enginesOk && flighPlanOk && navigationOk) { 
 console.log('All systems go, ready to launch:  ?'); 
 } else { 
 console.error('Abort the launch: '); 
 if (!enginesOk) { 
  console.error('engines not ready'); 
 } 
 if (!flighPlanOk) { 
  console.error('error found in flight plan'); 
 } 
 if (!navigationOk) { 
  console.error('error found in navigation systems'); 
 } 
 } 
} 
複製程式碼
  • 在工作區域執行 http-server 命令,你將會看到如下輸出

img

使用Promise.all收集多個結果

在上一小節中,我們一起學習瞭如何觸發多個非同步函式並等待多個非同步函式結果。上一節我們只使用了asyc/await,本節小編和大家一起使用Promise.all來收集多個非同步函式的結果,在某些情況下,儘量使用Promise相關的API,具體的程式碼如下:

  • 通過控制檯命令切換至工作區
  • 建立一個Promise-all-collect-concurrently的資料夾

建立三個函式功能checkEngines,checkFlightPlan,和checkNavigationSystem用來記錄資訊時,這三個函式都返回一個Promise,示例程式碼如下:

function checkEngines() { 
 console.log('checking engine'); 
 return new Promise(function (resolve) { 
 setTimeout(function() { 
  console.log('engine check completed'); 
  resolve(Math.random() < 0.9) 
 }, 250) 
 }); 
} 
function checkFlightPlan() { 
 console.log('checking flight plan'); 
 return new Promise(function (resolve) { 
 setTimeout(function() { 
  console.log('flight plan check completed'); 
  resolve(Math.random() < 0.9) 
 }, 350) 
 }); 
} 
function checkNavigationSystem() { 
 console.log('checking navigation system'); 
 return new Promise(function (resolve) { 
 setTimeout(function() { 
  console.log('navigation system check completed'); 
  resolve(Math.random() < 0.9) 
 }, 450) 
 }); 
} 
複製程式碼
  • 建立一個async的main函式呼叫上一步建立的函式。使用Promise.all收集多個結果,將結果返回給變數,程式碼實現如下:
export async function main() { 
 const prelaunchChecks = [ 
 checkEngines(), 
 checkFlightPlan(), 
 checkNavigationSystem() 
 ]; 
 const checkResults = await Promise.all(prelaunchChecks); 
 const readyToLaunch = checkResults.reduce((acc, curr) => acc && 
 curr); 
 if (readyToLaunch) { 
 console.log('All systems go, ready to launch: ?'); 
 } else { 
 console.error('Something went wrong, abort the launch: '); 
 } } 
} 
複製程式碼
  • 在工作區域執行 http-server 命令,你將會看到如下輸出:

img

Promise.all接收多個promise的陣列,並整體返回一個Promise,如果和上一小節的程式碼進行比較,程式碼量少了不少。

使用try-catch捕獲異常

並非所有的async都能成功返回,我們需要處理程式的異常,在本小節中,你將會看到如何使用try-catch捕獲async函式引發的異常,具體操作的流程如下:

  • 通過控制檯命令切換至工作區
  • 建立一個async-errors-try-catch的資料夾
  • 建立一個丟擲錯誤的async函式addBoosters
async function addBoosters() { 
 throw new Error('Unable to add Boosters'); 
} 
複製程式碼
  • 建立一個async函式,performGuidanceDiagnostic它也會丟擲一個錯誤:
async function performGuidanceDiagnostic (rocket) { 
 throw new Error('Unable to finish guidance diagnostic')); 
} 
複製程式碼
  • 建立一個async的main函式呼叫addBosters、performGuidanceDiagnostic兩個函式 ,使用try-catch捕獲異常:
 export async function main() { 
 console.log('Before Check'); 
 try { 
 await addBosters(); 
 await performGuidanceDiagnostic(); 
 } catch (e) { 
 console.error(e); 
 } 
} 
console.log('After Check'); 
複製程式碼
  • 在工作區域執行 http-server 命令,你將會看到如下輸出:

img

從輸出看出,我們使用熟悉的try-catch捕獲到了異常,如果第一個發生異常,第二個就不會執行,同時將會被記錄到,並輸出到控制檯,在下一小節,我們將一起學習如何使用try-catch捕獲Promise.all中執行的多個Promise的異常。

如何捕獲Promise.all中的異常

在上一小節,我們使用了Promise.all來收集多個非同步函式的結果。在收集異常方面,Promise.all更有趣。通常,我們在處理多個錯誤時,同時顯示多個錯誤資訊,我們必須編寫相關的業務邏輯。但是,在這小節,你將會使用Promise.all和try-catch捕獲異常,無需編寫複雜的布林邏輯處理業務,具體如何實現示例如下:

  • 通過控制檯命令切換至工作區
  • 建立一個Promise-all-collect-concurrently的資料夾
  • 建立三個async函式checkEngines,checkFlightPlan以及checkNavigationSystem用來記錄資訊時,返回Promise,一個成功的值的資訊和一個失敗值的資訊:
function checkEngines() { 
  console.log('checking engine'); 
 
  return new Promise(function (resolve, reject) { 
    setTimeout(function () { 
      if (Math.random() > 0.5) { 
        reject(new Error('Engine check failed')); 
      } else { 
        console.log('Engine check completed'); 
        resolve(); 
      } 
    }, 250) 
  }); 
} 
 
function checkFlightPlan() { 
  console.log('checking flight plan'); 
 
  return new Promise(function (resolve, reject) { 
    setTimeout(function () { 
      if (Math.random() > 0.5) { 
        reject(new Error('Flight plan check failed')); 
      } else { 
        console.log('Flight plan check completed'); 
        resolve(); 
      } 
    }, 350) 
  }); 
} 
 
function checkNavigationSystem() { 
  console.log('checking navigation system'); 
  return new Promise(function (resolve, reject) { 
    setTimeout(function () { 
      if (Math.random() > 0.5) { 
        reject(new Error('Navigation system check failed')); 
      } else { 
        console.log('Navigation system check completed'); 
        resolve(); 
      } 
    }, 450) 
  }); 
} 
複製程式碼

建立一個async的main函式呼叫每個在上一步中建立的功能函式。等待結果,捕獲並記錄引發的異常。如果沒有丟擲異常,則記錄成功:

 export async function main() { 
 try { 
 const prelaunchChecks = [ 
  checkEngines, 
  checkFlightPlan, 
  checkNavigationSystem 
 ]; 
 await Promise.all(prelauchCheck.map((check) => check()); 
 console.log('All systems go, ready to launch: ?'); 
 } catch (e) { 
 console.error('Aborting launch: '); 
 console.error(e); 
 } 
 } 
} 
複製程式碼
  • 在工作區域執行 http-server 命令,你將會看到如下輸出

img

Promise.all返回一個Promise,當await在錯誤狀態下,會丟擲異常。三個非同步promise同時執行,如果其中一個或多個錯誤得到滿足,則會丟擲一個或多個錯誤;

你會發現只有一個錯誤會被記錄下來,與同步程式碼一樣,我們的程式碼可能會丟擲多個異常,但只有一個異常會被catch捕獲並記錄。

使用finally確保函式執行

錯誤處理可能會變得相當複雜。有些情況,其中你希望發生錯誤時繼續冒泡呼叫堆疊以便執行其它更高階別處理。在這些情況下,您可能還需要執行一些清理任務。本小節,你將瞭解如何使用finally以確保執行某些程式碼,而不管錯誤狀態如何,具體如何實現示例如下:

  • 通過控制檯命令切換至工作區
  • 建立一個Promise-all-collect-concurrently的資料夾
  • 建立三個async函式checkEngines,checkFlightPlan、checkNavigationSystem用來記錄資訊,返回Promise,一個成功的值的資訊和一個失敗值的資訊:
function checkEngines() { 
 console.log('checking engine'); 
 return new Promise(function (resolve, reject) { 
 setTimeout(function () { 
  if (Math.random() > 0.5) { 
  reject(new Error('Engine check failed')); 
  } else { 
  console.log('Engine check completed'); 
  resolve(); 
  } 
 }, 250) 
 }); 
} 
function checkFlightPlan() { 
 console.log('checking flight plan'); 
 return new Promise(function (resolve, reject) { 
 setTimeout(function () { 
  if (Math.random() > 0.5) { 
  reject(new Error('Flight plan check failed')); 
  } else { 
  console.log('Flight plan check completed'); 
  resolve(); 
  } 
 }, 350) 
 }); 
} 
function checkNavigationSystem() { 
 console.log('checking navigation system'); 
 return new Promise(function (resolve, reject) { 
 setTimeout(function () { 
  if (Math.random() > 0.5) { 
  reject(new Error('Navigation system check failed')); 
  } else { 
  console.log('Navigation system check completed'); 
  resolve(); 
  } 
 }, 450) 
 }); 
} 
複製程式碼
  • 建立一個asyncperformCheck函式,呼叫上一步中建立的每個函式。等待結果,並用於finally記錄完整的訊息:
async function performChecks() { 
 console.log('Starting Pre-Launch Checks'); 
 try { 
 const prelaunchChecks = [ 
  checkEngines, 
  checkFlightPlan, 
  checkNavigationSystem 
 ]; 
 return Promise.all(prelauchCheck.map((check) => check()); 
 } finally { 
 console.log('Completed Pre-Launch Checks'); 
 } 
 } 
複製程式碼
  • 建立一個async的main函式調該函式performChecks。等待結果,捕獲並記錄引發的異常。
export async function main() { 
 try { 
 await performChecks(); 
 console.log('All systems go, ready to launch: ?'); 
 } catch (e) { 
 console.error('Aborting launch: '); 
 console.error(e); 
 } 
} 
複製程式碼
  • 在工作區域執行 http-server 命令,你將會看到如下輸出

img

與上一小節一樣,異常在main函式中進行捕獲,由於finally的存在,讓我清楚的知道performChecks確保需要執行的輸出完成。你可以設想,處理錯誤是一個重要的任務,並且async/await允許我們使用try/catch的方式同時處理非同步和同步程式碼的錯誤,大大簡化了我們處理錯誤的工作量,讓程式碼更加簡潔。

用async/await改寫上篇文章Promise的例子

上篇文章《JavaScript基礎——Promise使用指南》的最後,我們使用Promise的方法改寫了回撥的例子,本文的最後,我們將用今天學到的內容async/await改寫這個例子,如何實現呢,程式碼如下:

const fs = require('fs'); 
const path = require('path'); 
const postsUrl = path.join(__dirname, 'db/posts.json'); 
const commentsUrl = path.join(__dirname, 'db/comments.json'); 
//return the data from our file 
function loadCollection(url) { 
 return new Promise(function(resolve, reject) { 
  fs.readFile(url, 'utf8', function(error, data) { 
   if (error) { 
    reject('error'); 
   } else { 
    resolve(JSON.parse(data)); 
   } 
  }); 
 }); 
} 
//return an object by id 
function getRecord(collection, id) { 
 return new Promise(function(resolve, reject) { 
  const data = collection.find(function(element){ 
   return element.id == id; 
  }); 
  resolve(data); 
 }); 
} 
//return an array of comments for a post 
function getCommentsByPost(comments, postId) { 
 return comments.filter(function(comment){ 
  return comment.postId == postId; 
 }); 
} 
async function getPost(){ 
 try { 
  const posts = await loadCollection(postsUrl); 
  const post = await getRecord(posts, "001"); 
  const comments = await loadCollection(commentsUrl); 
  const postComments = await getCommentsByPost(comments, post.id); 
  console.log(post); 
  console.log(postComments); 
 } catch (error) { 
  console.log(error); 
 } 
} 
getPost(); 
複製程式碼

和Promise的方式相比,async/await 的實現方式是不是更直觀更容易理解呢,讓我幾乎能用同步的方式編寫非同步程式碼。

此文已由作者授權騰訊雲+社群釋出


相關文章