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 版のテストを書く
であっさり対応完了。
オープンソースすごい。