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 の場合は cb
に context
にしている。)
そして、cb
が falsy だった場合に74行目で createPromiseCallback
から promise
と cb
を受け取っている。
その後の記述では処理の終了後に 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 対応できる。
まとめ
- コールバックを受け取る関数のテストを書く
- この手法で Promise 対応する
- Promise 版のテストを書く
であっさり対応完了。
オープンソースすごい。