用Async函數簡化異步代碼

Promise 在 JavaScript 上發布之初就在互聯網上流行了起來 — 它們幫開發人員擺脫了回調地獄,解決了在很多地方困擾 JavaScript 開發者的異步問題。但 Promises 也遠非完美。它們一直請求回調,在一些復雜的問題上仍會有些雜亂和一些難以置信的冗余。

隨著 ES6 的到來(現在被稱作 ES2015),除了引入 Promise 的規范,不需要請求那些數不盡的庫之外,我們還有了生成器。生成器可在函數內部停止執行,這意味著可把它們封裝在一個多用途的函數中,我們可在代碼移動到下一行之前等待異步操作完成。突然你的異步代碼可能就開始看起來同步了。

這只是第一步。異步函數因今年加入 ES2017,已進行標準化,本地支持也進一步優化。異步函數的理念是使用生成器進行異步編程,并給出他們自己的語義和語法。因此,你無須使用庫來獲取封裝的實用函數,因為這些都會在后臺處理。

運行文章中的 async/await 實例,你需要一個能兼容的瀏覽器。

運行兼容

在客戶端,Chrome、Firefox 和 Opera 能很好地支持異步函數。

從 7.6 版本開始,Node.js 默認啟用 async/await。

異步函數和生成器對比

這有個使用生成器進行異步編程的實例,用的是 Q 庫:

var doAsyncOp = Q.async(function* () { 
 
  var val = yield asynchronousOperation(); 
 
  console.log(val); 
 
  return val; 
 
});  

Q.async 是個封裝函數,處理場景后的事情。其中 * 表示作為一個生成器函數的功能,yield 表示停止函數,并用封裝函數代替。Q.async 將會返回一個函數,你可對它賦值,就像賦值 doAsyncOp 一樣,隨后再調用。

ES7 中的新語法更簡潔,操作示例如下:

async function doAsyncOp () { 
 
  var val = await asynchronousOperation();      
 
  console.log(val); 
 
  return val; 
 
};  

差異不大,我們刪除了一個封裝的函數和 * 符號,轉而用 async 關鍵字代替。yield 關鍵字也被 await 取代。這兩個例子事實上做的事是相同的:在 asynchronousOperation 完成之后,賦值給 val,然后進行輸出并返回結果。

將 Promises 轉換成異步函數

如果我們使用 Vanilla Promises 的話前面的示例將會是什么樣?

function doAsyncOp () { 
 
  return asynchronousOperation().then(function(val) { 
 
    console.log(val); 
 
    return val; 
 
  }); 
 
};  

這里有相同的代碼行數,但這是因為 then 和給它傳遞的回調函數增加了很多的額外代碼。另一個讓人厭煩的是兩個 return 關鍵字。這一直有些事困擾著我,因為它很難弄清楚使用 promises 的函數確切的返回是什么。

就像你看到的,這個函數返回一個 promises,將會賦值給 val,猜一下生成器和異步函數示例做了什么!無論你在這個函數返回了什么,你其實是暗地里返回一個 promise 解析到那個值。如果你根本就沒有返回任何值,你暗地里返回的 promise 解析為 undefined。

鏈式操作

Promise 之所以能受到眾人追捧,其中一個方面是因為它能以鏈式調用的方式把多個異步操作連接起來,避免了嵌入形式的回調。不過 async 函數在這個方面甚至比 Promise 做得還好。

下面演示了如何使用 Promise 來進行鏈式操作(我們只是簡單的多次運行 asynchronousOperation 來進行演示)。

function doAsyncOp() { 
 
  return asynchronousOperation() 
 
    .then(function(val) { 
 
      return asynchronousOperation(val); 
 
    }) 
 
    .then(function(val) { 
 
      return asynchronousOperation(val); 
 
    }) 
 
    .then(function(val) { 
 
      return asynchronousOperation(val); 
 
    }); 
 
}  

使用 async 函數,只需要像編寫同步代碼那樣調用 asynchronousOperation:

async function doAsyncOp () { 
 
  var val = await asynchronousOperation(); 
 
  val = await asynchronousOperation(val); 
 
  val = await asynchronousOperation(val); 
 
  return await asynchronousOperation(val); 
 
};  

甚至最后的 return 語句中都不需要使用 await,因為用或不用,它都返回了包含了可處理終值的 Promise。

并發操作

Promise 還有另一個偉大的特性,它們可以同時進行多個異步操作,等他們全部完成之后再繼續進行其它事件。ES2015 規范中提供了 Promise.all(),就是用來干這個事情的。

這里有一個示例:

function doAsyncOp() { 
 
  return Promise.all([ 
 
    asynchronousOperation(), 
 
    asynchronousOperation() 
 
  ]).then(function(vals) { 
 
    vals.forEach(console.log); 
 
    return vals; 
 
  }); 
 
}  

Promise.all() 也可以當作 async 函數使用:

async function doAsyncOp() { 
 
  var vals = await Promise.all([ 
 
    asynchronousOperation(), 
 
    asynchronousOperation() 
 
  ]); 
 
  vals.forEach(console.log.bind(console)); 
 
  return vals; 
 
}  

這里就算使用了 Promise.all,代碼仍然很清楚。

處理拒絕

Promises 可以被接受(resovled)也可以被拒絕(rejected)。被拒絕的 Promise 可以通過一個函數來處理,這個處理函數要傳遞給 then,作為其第二個參數,或者傳遞給 catch 方法。現在我們沒有使用 Promise API 中的方法,應該怎么處理拒絕?可以通過 try 和 catch 來處理。使用 async 函數的時候,拒絕被當作錯誤來傳遞,這樣它們就可以通過 JavaScript 本身支持的錯誤處理代碼來處理。

function doAsyncOp() { 
 
  return asynchronousOperation() 
 
    .then(function(val) { 
 
      return asynchronousOperation(val); 
 
    }) 
 
    .then(function(val) { 
 
      return asynchronousOperation(val); 
 
    }) 
 
    .catch(function(err) { 
 
      console.error(err); 
 
    }); 
 
}  

這與我們鏈式處理的示例非常相似,只是把它的最后一環改成了調用 catch。如果用 async 函數來寫,會像下面這樣。

async function doAsyncOp () { 
 
  try { 
 
    var val = await asynchronousOperation(); 
 
    val = await asynchronousOperation(val); 
 
    return await asynchronousOperation(val); 
 
  } catch (err) { 
 
    console.err(err); 
 
  } 
 
};  

它不像其它往 async 函數的轉換那樣簡潔,但是確實跟寫同步代碼一樣。如果你在這里不捕捉錯誤,它會延著調用鏈一直向上拋出,直到在某處被捕捉處理。如果它一直未被捕捉,它最終會中止程序并拋出一個運行時錯誤。Promise 以同樣的方式運作,只是拒絕不必當作錯誤來處理;它們可能只是一個說明錯誤情況的字符串。如果你不捕捉被創建為錯誤的拒絕,你會看到一個運行時錯誤,不過如果你只是使用一個字符串,會失敗卻不會有輸出。

中斷 Promise

拒絕原生的 Promise,只需要使用 Promise 構建函數中的 reject 就好,當然也可以直接拋出錯誤——在 Promise 的構造函數中,在 then 或 catch 的回調中拋出都可以。如果是在其它地方拋出錯誤,Promise 就管不了了。

這里有一些拒絕 Promise 的示例:

function doAsyncOp() { 
 
  return new Promise(function(resolve, reject) { 
 
    if (somethingIsBad) { 
 
      reject("something is bad"); 
 
    } 
 
    resolve("nothing is bad"); 
 
  }); 
 
} 
 
  
 
/*-- or --*/ 
 
  
 
function doAsyncOp() { 
 
  return new Promise(function(resolve, reject) { 
 
    if (somethingIsBad) { 
 
      reject(new Error("something is bad")); 
 
    } 
 
    resolve("nothing is bad"); 
 
  }); 
 
} 
 
  
 
/*-- or --*/ 
 
  
 
function doAsyncOp() { 
 
  return new Promise(function(resolve, reject) { 
 
    if (somethingIsBad) { 
 
      throw new Error("something is bad"); 
 
    } 
 
    resolve("nothing is bad"); 
 
  }); 
 
}  

一般來說,最好使用 new Error,因為它會包含錯誤相關的其它信息,比如拋出位置的行號,以及可能會有用的調用棧。

這里有一些拋出 Promise 不能捕捉的錯誤的示例:

function doAsyncOp() { 
 
  // the next line will kill execution 
 
  throw new Error("something is bad"); 
 
  return new Promise(function(resolve, reject) { 
 
    if (somethingIsBad) { 
 
      throw new Error("something is bad"); 
 
    } 
 
    resolve("nothing is bad"); 
 
  }); 
 
} 
 
  
 
// assume `doAsyncOp` does not have the killing error 
 
function x() { 
 
  var val = doAsyncOp().then(function() { 
 
    // this one will work just fine 
 
    throw new Error("I just think an error should be here"); 
 
  }); 
 
  // this one will kill execution 
 
  throw new Error("The more errors, the merrier"); 
 
  return val; 
 
}  

在 async 函數的 Promise 中拋出錯誤就不會產生有關范圍的問題——你可以在 async 函數中隨時隨地拋出錯誤,它總會被 Promise 抓住:

async function doAsyncOp() { 
 
  // the next line is fine 
 
  throw new Error("something is bad"); 
 
  if (somethingIsBad) { 
 
    // this one is good too 
 
    throw new Error("something is bad"); 
 
  } 
 
  return "nothing is bad"; 
 
}  
 
  
 
// assume `doAsyncOp` does not have the killing error 
 
async function x() { 
 
  var val = await doAsyncOp(); 
 
  // this one will work just fine 
 
  throw new Error("I just think an error should be here"); 
 
  return val; 
 
}  

當然,我們永遠不會運行到 doAsyncOp 中的第二個錯誤,也不會運行到 return 語句,因為在那之前拋出的錯誤已經中止了函數運行。

問題

如果你剛開始使用 async 函數,需要小心嵌套函數的問題。比如,如果你的 async 函數中有另一個函數(通常是回調),你可能認為可以在其中使用 await ,但實際不能。你只能直接在 async 函數中使用 await 。

比如,這段代碼無法運行:

async function getAllFiles(fileNames) { 
 
  return Promise.all( 
 
    fileNames.map(function(fileName) { 
 
      var file = await getFileAsync(fileName); 
 
      return parse(file); 
 
    }) 
 
  ); 
 
}  

第 4 行的 await 無效,因為它是在一個普通函數中使用的。不過可以通過為回調函數添加 async 關鍵字來解決這個問題。

async function getAllFiles(fileNames) { 
 
  return Promise.all( 
 
    fileNames.map(async function(fileName) { 
 
      var file = await getFileAsync(fileName); 
 
      return parse(file); 
 
    }) 
 
  ); 
 
}  

你看到它的時候會覺得理所當然,即便如此,仍然需要小心這種情況。

也許你還想知道等價的使用 Promise 的代碼:

function getAllFiles(fileNames) { 
 
  return Promise.all( 
 
    fileNames.map(function(fileName) { 
 
      return getFileAsync(fileName).then(function(file) { 
 
        return parse(file); 
 
      }); 
 
    }) 
 
  ); 
 
}  

接下來的問題是關于把 async 函數看作同步函數。需要記住的是,async 函數內部的的代碼是同步運行的,但是它會立即返回一個 Promise,并繼續運行外面的代碼,比如:

var a = doAsyncOp(); // one of the working ones from earlier 
 
console.log(a); 
 
a.then(function() { 
 
  console.log("`a` finished"); 
 
}); 
 
console.log("hello"); 
 
  
 
/* -- will output -- */ 
 
Promise Object 
 
hello 
 
`a` finished  

你會看到 async 函數實際使用了內置的 Promise。這讓我們思考 async 函數中的同步行為,其它人可以通過普通的 Promise API 調用我們的 async 函數,也可以使用它們自己的 async 函數來調用。

如今,更好的異步代碼!

即使你本身不能使用異步代碼,你也可以進行編寫或使用工具將其編譯為 ES5。 異步函數能讓代碼更易于閱讀,更易于維護。 只要我們有 source maps,我們可以隨時使用更干凈的 ES2017 代碼。

有許多可以將異步功能(和其他 ES2015+功能)編譯成 ES5 代碼的工具。 如果您使用的是 Babel,這只是安裝 ES2017 preset 的例子。

 

來自:http://developer.51cto.com/art/201704/537448.htm

 

 本文由用戶 tzvt8047 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!