async/awaitを使ったモダンな非同期処理

2017年6月リリースのES2017で、JavaScriptはようやくまともな非同期処理を手に入れました。

以下はMODERN ASYNCHRONOUS JAVASCRIPT WITH ASYNC AND AWAITの日本語訳です。


MODERN ASYNCHRONOUS JAVASCRIPT WITH ASYNC AND AWAIT

JavaScriptで非同期処理を行う近代的な方法とは。


Introduction

JavaScriptはコールバック地獄からES2015のPromiseまで瞬く間に進化しました。

そしてES2017では、async/awaitによってより簡潔に非同期処理を書けるようになりました。

非同期関数はPromiseとジェネレータの合わせ技であり、そしてPromiseより高いレベルの抽象化です。

リピートミー。「async/awaitはPromiseで作られている」。


Why was async/await introduced?

async/awaitによってPromiseまわりの定型文を減らすことができ、さらにPromiseの制限を緩和します。

ES2015でPromiseが導入されたとき、これは非同期関連のコードの問題を解決することを目的としていました。

しかしES2017が導入されるまでの2年間で、Promiseは最終解決にはほど遠いことが明らかになりました。

Promiseは有名なコールバック地獄の問題を解決するために導入されましたが、しかしPromiseはPromise自身の複雑さと、構文の複雑さをコードにもたらしました。

Promiseは結果的に、開発者によりよい構文を考える機会を与え、そして我々はついにasyncを手に入れました。

Promiseはコードを同期処理のように見せかけますが、実際は非同期であり、処理がブロックされることはありません。


How it works

async関数はPromiseを返します。

function doSomethingAsync() {

return new Promise((resolve) => {
setTimeout(() => resolve('I did something'), 3000)
})
}

この関数を呼び出すときにawaitを付けると、このコードはPromiseがresolvedかrejectedを返すまで停止します。

さらに関数にはasyncを付けておかなければなりません。

async function doSomething() {

console.log(await doSomethingAsync())
}


A quick example

以下はasync/awaitを使った、非同期関数の簡単な例です。

function doSomethingAsync() {

return new Promise((resolve) => {
setTimeout(() => resolve('I did something'), 3000)
})
}

async function doSomething() {
console.log(await doSomethingAsync())
}

console.log('Before')
doSomething()
console.log('After')

上記のコードのコンソール出力は以下になります。

Before

After
I did something // 3秒後


Promise all the things

関数にasyncキーワードを付けると、その関数はPromiseを返すようになります。

たとえ明示しなくても、内部的には勝手にPromiseを返すようになります。

そのため、以下のようなコードは有効です。

async function aFunction() {

return 'test';
}

aFunction().then(alert); // 'test'のアラートが出る

上記のコードは以下と同値です。

async function aFunction() {

return Promise.resolve('test');
}

aFunction().then(alert);


The code is much simpler to read

上記例のように、async/awaitを使ったコードは非常に簡潔です。

メソッドチェーンとコールバックだけを使った素のPromiseと比較してみてください。

上記はとてもシンプルな例でしたが、コードが複雑になるとasync/awaitの利点はますます大きくなります。

たとえば、JSONリソースを取得してパースする関数を、Promiseで書くと以下のようになります。

const getFirstUserData = () => {

return fetch('/users.json') // ユーザ一覧のJSONを取得
.then(response => response.json()) // パース
.then(users => users[0]) // 最初のユーザを取り出す
.then(user => fetch(`/users/${user.name}`)) // 指定ユーザのJSONを取得
.then(userResponse => response.json()) // パース
}

getFirstUserData();

同じ機能をasync/awaitを使って書くとこうなります。

async function getFirstUserData() {

const response = await fetch('/users.json') // ユーザ一覧のJSONを取得
const users = await response.json() // パース
const user = users[0] // 最初のユーザを取り出す
const userResponse = await fetch(`/users/${user.name}`); // 指定ユーザのJSONを取得
const userData = await user.json(); // パース
return userData
}

getFirstUserData();


Multiple async functions in serie

非同期関数のチェーンも簡単に書くことができ、構文も素のPromiseよりはるかにわかりやすくなります。

function promiseToDoSomething() {

return new Promise((resolve)=>{
setTimeout(() => resolve('I did something'), 10000)
})
}

async function watchOverSomeoneDoingSomething() {
const something = await promiseToDoSomething()
return something + ' and I watched'
}

async function watchOverSomeoneWatchingSomeoneDoingSomething() {
const something = await watchOverSomeoneDoingSomething()
return something + ' and I watched as well'
}

watchOverSomeoneWatchingSomeoneDoingSomething().then((res) => {
console.log(res)
})

出力はI did something and I watched and I watched as wellです。


感想

つい最近までまともに非同期処理が書けなかったことにびっくりだよ。

のっけから"JavaScript evolved in a very short time from callbacks to promises"って書いてあるんだけどそうだったっけ?

XMLHttpRequest.onreadystatechangeからPromiseまで何億年かかったと思ってるんだ。

"from promises to async/await"ならまだわかるけどさ。

ちなみにPHPなら何も考えなくても同期になります。やったね。

    longTimeProcess1(); // 終わるまで次に進まない

longTimeProcess2(); // 終わるまで次に進まない
longTimeProcess3(); // 終わるまで次に進まない

なお、非同期処理が超辛い模様。