Promise は、JavaScript で最初にリリースされたときにインターネットで人気になりました。これは、開発者がコールバック地獄から抜け出し、多くの場所で JavaScript 開発者を悩ませている非同期の問題を解決するのに役立ちます。しかし、Promise は完璧には程遠いです。彼らはコールバックを要求し続けますが、依然として少し面倒で、いくつかの複雑な問題では信じられないほど冗長である可能性があります。
ES6 (現在は ES2015 と呼ばれています) の登場により、Promise 仕様の導入に加えて、それらの無数のライブラリをリクエストする必要がなくなり、ジェネレーター も追加されました。ジェネレーターは function 内で実行を停止できます。つまり、ジェネレーターを多目的関数でラップすることができ、コードの次の行に進む前に非同期操作が完了するのを待つことができます。突然、非同期コードが同期しているように見える場合があります。
これは最初のステップにすぎません。非同期関数は今年 ES2017 に追加されてから標準化され、ローカル サポートがさらに最適化されました。 async 関数の考え方は、非同期プログラミングにジェネレーターを使用し、独自のセマンティクスと構文を与えることです。したがって、カプセル化されたユーティリティ関数はバックグラウンドで処理されるため、ライブラリを使用して取得する必要はありません。
この記事の 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 は stop 関数を表し、ラッパー関数に置き換えられます。 Q.async は、doAsyncOp と同様に、値を割り当てて呼び出すことができる関数を返します。
ES7 の新しい構文はより簡潔で、操作例は次のとおりです:
async function doAsyncOp () { var val = await asynchronousOperation(); console.log(val); return val; };
違いは大きくありません。カプセル化された関数と * シンボルを削除し、代わりに async キーワードを使用します。 yield キーワードも await に置き換えられました。これら 2 つの例は実際には同じことを行います。asynchronousOperation が完了した後、値を val に代入し、結果を出力して返します。
Promise を非同期関数に変換する
Vanilla Promises を使用した場合、前の例はどのようになりますか?
function doAsyncOp () { return asynchronousOperation().then(function(val) { console.log(val); return val; }); };
ここには同じ数のコード行がありますが、これは then とそれに渡される コールバックのためです。関数 は多くの余分なコードを追加します。もう 1 つの厄介な点は、2 つの return キーワードです。これは私にとって常に気になることでした。Promise を使用した関数が正確に何を返すかを理解するのは難しいからです。
ご覧のとおり、この関数は val に割り当てられる Promise を返します。この関数で何を返すかに関係なく、その値に密かに Promise を返していることになります。値をまったく返さない場合、返される Promise は暗黙的に未定義に解決されます。
連鎖操作
Promise が人々の間で人気がある理由の 1 つは、埋め込まれたコールバックを回避し、連鎖呼び出しで複数の非同期操作を接続できることです。ただし、この点では、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 を使用する必要はありません。これが使用されているかどうかに関係なく、処理可能なオブジェクトが返されるためです。 Final 値の約束。
同時実行操作
Promise のもう 1 つの優れた機能は、複数の非同期操作を同時に実行でき、すべての非同期操作が完了するのを待ってから、他の イベントを続行できることです。 ES2015 仕様では、これを行うために使用される Promise.all() が提供されています。
ここに例があります:
function doAsyncOp() { return Promise.all([ asynchronousOperation(), asynchronousOperation() ]).then(function(vals) { vals.forEach(console.log); return vals; }); }
Promise.all() は非同期関数としても使用できます:
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 中国語 Web サイトの他の関連記事を参照してください。