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 的例子。
以上是用Async函數簡化非同步程式碼( JavaScript開發技巧)的詳細內容。更多資訊請關注PHP中文網其他相關文章!