an odd fellow

仕事のメモ

JavascriptのPromiseとasync/awaitについて調べる

背景

ReactでSPAを作っている。APIからデータを取得するときredux thunkとaxiosを使っているが、このときの処理がexampleを見て見よう見まねでやっていてよくわかっていない。

例えば投稿を取得するソースコードはこんな感じになっているが、return async とか axios.get のあとの then とかがわかっていない。

export const fetchPosts = () => {
  return async (dispatch, getState) => {
    const posts = getState().posts
    dispatch(actions.startRequest(posts))
    axios
      .get("http://example.com/posts")
      .then(response => {
        dispatch(
          actions.receiveData(
            posts,
            null,
            normalize(response.data, [schemas.post])
          )
        )
      })
      .catch(error => {
        dispatch(actions.receiveData(posts, error))
      })
      .finally(() => {
        dispatch(actions.finishRequest(posts))
      })
  }
}

これを理解するためにいくつかドキュメントにあたったり実験した結果をまとめる。

そもそも同期/非同期とは

この記事がわかりやすかった。

JavaScriptの同期、非同期、コールバック、プロミス辺りを整理してみる

console.log(1);
setTimeout(function(){console.log(2)}, 0);
console.log(3);

待ち時間がゼロなので、console.log(1)、console.log(2)、console.log(3)の順に実行されると勘違いしやすいです。 しかしあくまでlog(1)、setTimeout(〜略〜)、log(3)が順に、つまり同期的にキューに登録された後、setTimeout(〜略〜)が実行されたタイミングで、つまり非同期でlog(2)がキューに登録される

だから、出力は以下のようになる。

1
3
2

Promiseやasync/awaitを使うと

1
2
3

のように書いた順番通りに実行できるような仕組みのようだ。これを同期的と呼んでいる。

async/awaitの前にPromiseを理解するべきっぽい

適当にググっていると以下の記事を見つけた。そこにはこうある。

Promiseの使い方、それに代わるasync/awaitの使い方

async/awaitを理解するには、Promiseも知る必要がある。

なのでまずPromiseから調べる。

Promiseの基本的な使い方

コマンドライン引数がaかどうかでresolve、rejectを切り替える。

promise-basic.js

const promise = new Promise((resolve, reject) => {
  if (process.argv[2] === 'a') {
    resolve("GOOD")
  } else {
    reject("BAD")
  }
})

promise.then(value=>console.log(value)).catch(value=>console.log(value))

実行結果

$ node promise-basic.js a
GOOD
$ node promise-basic.js b
BAD

Promise.resolveでPromiseのインスタンスを作れる。

promise-basic2.js

promise = Promise.resolve("GOOD")

promise.then(value=>console.log(value)).catch(value=>console.log(value))

実行結果

$ node promise-basic2.js
GOOD

resolveとかrejectの結果を変数に束縛できるか?

こんなような感じで書いて変数hogeに"GOOD"とか"BAD"とか入って欲しい。

promise-bind.js

const promise = new Promise((resolve, reject) => {
  if (process.argv[2] === 'a') {
    resolve("GOOD")
  } else {
    reject("BAD")
  }
})

const hoge = promise.then(value=>value).catch(value=>value)
console.log(hoge)

実行結果

$ node promise-bind.js a
Promise { <pending> }

thencatch の中でごちゃごちゃと書くしか無いのかなあ…。

thenの中でreturnすれば外に出せるのでは説

thenreturn したら外に出せるのでは?と思ったけどダメだった。 promise-bind2.js

const promise = new Promise((resolve, reject) => {
  if (process.argv[2] === 'a') {
    resolve("GOOD")
  } else {
    reject("BAD")
  }
})

const hoge = (
  () => {promise.then(value=>return value).catch(value=> return value)}
  )()
console.log(hoge)

実行結果

$ node promise-bind2.js a   
/Users/roronya/src/github.com/roronya/async-await/promise-bind2.js:10
  () => {promise.then(value=>return value).catch(value=> return value)}
                             ^^^^^^

SyntaxError: Unexpected token return
    at new Script (vm.js:51:7)
    at createScript (vm.js:136:10)
    at Object.runInThisContext (vm.js:197:10)
    at Module._compile (internal/modules/cjs/loader.js:618:28)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:665:10)
    at Module.load (internal/modules/cjs/loader.js:566:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:506:12)
    at Function.Module._load (internal/modules/cjs/loader.js:498:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:695:10)
    at startup (internal/bootstrap/node.js:201:19)

resolveとかrejectとか書かなかったらどうなるのか?

何も起こらなかった。

promise-not-resolve.js

const promise = new Promise((resolve, reject) => "GOOD")

promise.then(value=>console.log(value)).catch(value=>console.log(value))

実行結果

$ node promise-not-resolve.js
# 何も出ない

Promiseを使った感想

  • Promiseの内部で使った値を呼び出し側に出せないのが使いづらい
    • もし値を使いたいなら関数を呼んでその引数にするしか無さそう
  • Promise.resolveは使い所無い気がする
  • Promiseが返ってくる関数は、resolveやrejectに何が渡されるのか実装によって調べる必要があるっぽい

async/awaitの使い方

asyncだけで使う

関数にasyncと付けると Promise.resolve(関数の返り値) と同じ結果になるっぽい。 async-basic.js

const f = async () => "GOOD"

console.log(f)
f().then(value => console.log(value))

実行結果

$ node async-basic.js
[AsyncFunction: f]
GOOD

asyncの中でだけawaitが使える

await-basic.js

const f = async () => "GOOD"
const g = async () => {
  const y = await f()
  console.log('inner', y)
  return y
}

g().then(value => console.log('outer', value))

実行結果

$ node await-basic.js
inner GOOD
outer GOOD

classメソッドでもasync使える?

使える

async-class.js

class C {
  async f() {
    return "GOOD"
  }
}

const c = new C()
console.log(c.f)
c.f().then(v => console.log(v))

実行結果

[AsyncFunction: f]
GOOD

async/await使った感想

  • 受け取る側でPromiseを処理すればasyncは気軽に付けてよい
  • await付けるとPromiseの中の値を外に持ってこれる
    • が、結局asyncによってPromiseに包まれるから、もともとの呼び出し側はPromise.then.catchは書かないとダメっぽい