LoginSignup
6
4

More than 3 years have passed since last update.

Promiseを一回だけ実行する(非同期メモ化)

Last updated at Posted at 2019-09-20
class X {
  async init() {
  }
}

みたいなPromiseがあったとき、これを一回だけ実行したい。普通に考えると、initの結果をキャッシュするメモ化を行いたくなる。たとえば、以下のコード。

// Bad Pattern
class X {
  async init() {
      if (!this.initResult) {
        this.initResult = await this._init();
      }
      return this.initResult;
  }
  async _init() {
  }
}

これだと、initの実行中に別のinitが開始されると複数回実行されてしまう。

こういう場合には、定石があって、

class X {
  init() {
      if (!this.initPromise) {
        this.initPromise = this._init();
      }
      return this.initPromise;
  }
  async _init() {
  }
}

とする。あまり変わらないようだけど、asyncが外れたことで、メモ化の対象がPromiseになったことがポイント。この場合、_initは必ず一回だけ実行される。Promiseは、pendingやresolved状態で、何度でもresolveできるので、この性質を使っている。自分は、最初、この挙動を理解するのに結構戸惑ったけど、最近やっと慣れてきた。

さて、実装としては、これで良いのだけど、このためだけに、_initとか、initPromiseとか本質的ではない名前が増えるのは嫌だ。そういうときのためにdecoratorというものがある。

function memoize(target, fieldName, descriptor) {
  const func = descriptor.value;
  const memoizedFieldName = Symbol(`$$once_${fieldName}`);
  return Object.assign({}, descriptor, {
    value: function() {
      if (!this[memoizedFieldName]) {
        this[memoizedFieldName] = func.apply(this);
      }
      return this[memoizedFieldName];
    },
  });
}

class X {
 @once
  async init() {
  }
}

前のコードで、_initに相当するフィールドは、クロージャーに隠蔽され、initPromiseに相当するフィールドは、シンボルで隠蔽されてるので、Xの名前空間は何も汚染せず、整理整頓された気分になる。

onceという名前にしたけど、内部の実装はただのメモ化で、同期処理のメモ化にも使える。実務上は、以下のようなライブラリを使えば良い。

ちなみに、「非同期メモ化」で検索したら、4年前に自分が書いた記事が出てきた。

やりたいことは同じなんだけど、今回紹介した、Promiseを使ったやり方の方がエレガントでコード量も少ないので、前に書いたやり方を使うべき状況はないと思う。4年前にはPromiseはすでに一般的だったと記憶しているけど、その時点で、自分はPromiseのことを良く理解してなかったらしい。黒歴史だ。

6
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4