ymmooot

Vue にならうコールバックを受け取る関数の Promise 対応

Vue のコードを読んでるときに、おっと思ったコードが実際に実務で使えた。

概要

「コールバックを引数に受け取って、何か処理をしたあとにそれを実行する」といった古き良き関数があるとする。
が、async/await で書きたくなったときに、コールバックを受け取りつつも Promise を返すように改修するにはどうするか。
Vue に良いコードがあった。

create-renderer

それがこれ: github.com/vuejs/vue/blob/dev/src/server/create-renderer.js#L73-L76

renderToString という関数は第3引数に cb を受け取る。
(第2引数に context を受け取り、context が function の場合は cbcontext にしている。)

そして、cb が falsy だった場合に74行目で createPromiseCallback から promisecb を受け取っている。
その後の記述では処理の終了後に cb を呼び出しており、最後に promise を返している。

コールバックを渡した場合

コールバックを cb もしくは context として渡した場合、当然ながら cb にはコールバックが入っており、promise には何も代入されず undefined のまま。
なので、cb を実行すればコールバックが呼ばれるし、最終的には undefined をリターンしているだけ。

コールバックを渡さなかった場合

コールバックを渡さなかった場合、createPromiseCallback が呼ばれるので中身をみる。
こちら: github.com/vuejs/vue/blob/dev/src/server/util.js#L7

util.jsexport function createPromiseCallback () {
  let resolve, reject
  const promise: Promise<string> = new Promise((_resolve, _reject) => {
    resolve = _resolve
    reject = _reject
  })
  const cb = (err: Error, res?: string) => {
    if (err) return reject(err)
    resolve(res || '')
  }
  return { promise, cb }
}

9行目で作った Promise の resolve と reject を外のスコープに漏らし、それを使って cb という「err があれば reject し、なければ resolve する関数」を作っている。
つまり、createPromiseCallback から受けとった cb を実行すると、一緒にうけとった promise が resolve ないしは reject される。
その promise を return すれば、renderToString を使う側は await できるようになる。

これにより、コールバックを受け取ったときと、受け取らなかったときで createPromiseCallback の以降の処理をかき分ける必要がなくなる。
既存のロジックに手を入れずに、Promise 対応できる。

まとめ

  1. コールバックを受け取る関数のテストを書く
  2. この手法で Promise 対応する
  3. Promise 版のテストを書く

であっさり対応完了。
オープンソースすごい。