如何使用Await減少回調嵌套
在開發的時候,有時候需要發很多請求,然后經常會面臨嵌套回調的問題,即在一個回調里面又嵌了一個回調,導致代碼層層縮進得很厲害,如下代碼所示:
- ajax({
- url: "/list",
- type: "GET",
- success: function(data) {
- appendToDOM(data);
- ajax({
- url: "/update",
- type: "POST",
- success: function(data) {
- util.toast("Success!");
- })
- });
- }
- });
這樣的代碼看起來有點吃力,這種異步回調通常可以用Promise優化一下,可以把上面代碼改成:
- new Promise(resolve => {
- ajax({
- url: "/list",
- type: "GET",
- success: data => resolve(data);
- })
- }).then(data => {
- appendToDOM(data);
- ajax({
- url: "/update",
- type: "POST",
- success: function(data) {
- util.toast("Successfully!");
- })
- });
- });
Promise提供了一個resolve,方便通知什么時候異步結束了,不過本質還是一樣的,還是使用回調,只是這個回調放在了then里面。
當需要獲取多次異步數據的時候,可以使用Promise.all解決:
- let orderPromise = new Promise(resolve => {
- ajax("/order", "GET", data => resolve(data));
- });
- let userPromise = new Promise(resolve => {
- ajax("/user", "GET", data => resolve(data));
- });
- Promise.all([orderPromise, userPromise]).then(values => {
- let order = values[0],
- user = values[1];
- });
但是這里也是使用了回調,有沒有比較優雅的解決方式呢?
ES7的await/async可以讓異步回調的寫法跟寫同步代碼一樣。***個嵌套回調的例子可以用await改成下面的代碼:
- // 使用await獲取異步數據
- let leadList = await new Promise(resolve => {
- ajax({
- url: "/list",
- type: "GET",
- success: data => resolve(data);
- });
- });
- // await讓代碼很自然地像瀑布流一樣寫下來
- appendToDom(leadList);
- ajax({
- url: "/update",
- type: "POST",
- success: () => util.toast("Successfully");
- });
Await讓代碼可以像瀑布流一樣很自然地寫下來。
第二個例子:獲取多次異步數據,可以改成這樣:
- let order = await new Promise(
- resolve => ajax("/order", data => resovle(data))),
- user = await new Promise(
- resolve => ajax("/user", data => resolve(data)));
- // do sth. with order/user
這種寫法就好像從本地獲取數據一樣,就不用套回調函數了。
Await除了用在發請求之外,還適用于其它異步場景,例如我在創建訂單前先彈一個小框詢問用戶是要創建哪種類型的訂單,然后再彈具體的設置訂單的框,所以按正常思路這里需要傳遞一個按鈕回調的點擊函數,如下圖所示:
但其實可以使用await解決,如下代碼所示:
- let quoteHandler = require("./quote");
- // 彈出框詢問用戶并得到用戶的選擇
- let createType = await quoteHandler.confirmCreate();
quote里面返回一個Promise,監聽點擊事件,并傳遞createType:
- let quoteHandler = {
- confirmCreate: function(){
- dialog.showDialog({
- contentTpl: tpl,
- className: "confirm-create-quote"
- });
- let $quoteDialog = $(".confirm-create-quote form")[0];
- return new Promise(resolve => {
- $(form.submit).on("click", function(event){
- resolve(form.createType.value);
- });
- });
- }
- }
這樣外部調用者就可以使用await,而不用傳遞一個點擊事件的回調函數了。
但是需要注意的是await的一次性執行特點。相對于回調函數來說,await的執行是一次性的,例如監聽點擊事件,然后使用await,那么點擊事件只會執行一次,因為代碼從上往下執行完了,所以當希望點擊之后出錯了還能繼續修改和提交就不能使用await,另外使用await獲取異步數據,如果出錯了,那么成功的resolve就不會執行,后續的代碼也不會執行,所以請求出錯的時候基本邏輯不會有問題。
要在babel里面使用await,需要:
(1)安裝一個Node包
- npm install –save-dev babel-plugin-transform-async-to-generator
(2)在工程的根目錄添加一個.babelrc文件,內容為:
- {
- "plugins": ["transform-async-to-generator"]
- }
(3)使用的時候先引入一個模塊
- require("babel-polyfill");
然后就可以愉快地使用ES7的await了。
使用await的函數前面需要加上async關鍵字,如下代碼:
- async showOrderDialog() {
- // 獲取創建類型
- let createType = await quoteHandler.confirmCreate();
- // 獲取老訂單數據
- let orderInfo = await orderHandler.getOrderData();
- }
我們再舉一個例子:使用await實現JS版的sleep函數,因為原生是沒有提供線程休眠函數的,如下代碼所示:
- function sleep (time) {
- return new Promise(resolve =>
- setTimeout(() => resolve(), time));
- }
- async function start () {
- await sleep(1000);
- }
- start();
babel的await實現是轉成了ES6的generator,如下關鍵代碼:
- while (1) {
- switch (_context.prev = _context.next) {
- case 0:
- _context.next = 2;
- // sleep返回一個Promise對象
- return sleep(1000);
- case 2:
- case "end":
- return _context.stop();
- }
- }
而babel的generator也是要用ES5實現的,什么是generator呢?如下圖所示:
生成器用function*定義,每次執行生成器的next函數的時候會返回當前生成器里用yield返回的值,然后生成器的迭代器往后走一步,直到所有yield完了。
有興趣的可以繼續研究babel是如何把ES7轉成ES5的,據說原生的實現還是直接基于Promise.
使用await還有一個好處,可以直接try-catch捕獲異步過程拋出的異常,因為我們是不能直接捕獲異步回調里面的異常的,如下代碼:
- let quoteHandler = {
- confirmCreate: function(){
- $(form.submit).on("click", function(event){
- // 這里會拋undefined異常:訪問了undefined的value屬性
- callback(form.notFoundInput.value);
- });
- }
- }
- try {
- // 這里無法捕獲到異常
- quoteHandler.confirmCreate();
- } catch (e) {
- }
上面的try-catch是沒有辦法捕獲到異常的,因為try里的代碼已經執行完了,在它執行的過程中并沒有異常,因此無法在這里捕獲,如果使用Promise的話一般是使用Promise鏈的catch:
- let quoteHandler = {
- confirmCreate: function(){
- return new Promise(resolve => {
- $(form.submit).on("click", function(event){
- // 這里會拋undefined異常:訪問了undefined的value屬性
- resolve(form.notFoundInput.value);
- });
- });
- }
- }
- quoteHandler.confirmCreate().then(createType => {
- }).catch(e => {
- // 這里能捕獲異常
- });
而使用await,我們可以直接用同步的catch,就好像它真的變成同步執行了:
- try {
- createType = await quoteHandler.confirmCreate("order");
- }catch(e){
- console.log(e);
- return;
- }
總之使用await讓代碼少寫了很多嵌套,很方便的邏輯處理,縱享絲滑。
原文鏈接:https://fed.renren.com/2017/10/31/await/
【本文是51CTO專欄作者“人人網FED”的原創稿件,轉載請通過51CTO聯系原作者獲取授權】