🐙

JS基礎いろいろーPromise後編

2022/03/19に公開

前編ではPromiseとJSの非同期処理について紹介しました。後編では、自作のPromiseを実装してみたいと思います。今回の内容は、Promiseの特徴についてすでに理解があるとの前提です。

自作プロミスの枠組み

Promiseの特徴に基づいて、自作Promiseの枠をまず定義します。

class MyPromise {
  constructor(executor) {
    // 関数以外の引数はエラーを出す
    if (!(executor instanceof Function)) {
      throw new Error('Require a function to init MyPromise')
    }
    // 初期ステートをpending、値をundefinedに
    this.state = 'pending'
    this.value = undefined
  }

  resolve() {}

  reject() {}
}

つまり、関数を引数として、初期のステートはpending、値はundefinedで、もちろん、resolverejectメソッドがあります。

この状態でnewでインスタンスを作ると:

let p = new MyPromise(()=>{})
console.log(p)
// {state: 'pending', value: undefined}

コンストラクター関数では、この二つの関数を引数として、executor関数を実行します。次に、resolveとrejectによる状態変更を追加します。

  constructor(executor) {
    // 関数以外の引数はエラーを出す
    if (!(executor instanceof Function)) {
      throw new Error('Require a function to init MyPromise')
    }
    // 初期ステートをpending、値をundefinedに
    this.state = 'pending'
    this.value = undefined
    // executor関数を実行、ここでコンテキストのthisをプロミスインスタンスにバインドする
    try {
      executor(this.resolve.bind(this), this.reject.bind(this))
    } catch (e) {
      this.reject(e)
    }  
  }

  // pending状態から変更可能
  resolve(val) {
    if (this.state === 'pending') {
      this.state = 'fulfilled'
      this.value = val
    }
  }

  reject(val) {
    if (this.state === 'pending') {
      this.state = 'rejected'
      this.value = val
    }
  }

簡単なthenメソッド

thenメソッドには、onFulfilledonRejectedの二つの関数を引数とし、現在のプロミスの状態がpendingから変更するときに、対応する関数を実行する。これを元に実現してみると:

class MyPromise {
  // ...
  // then関数は、onFulfilledとonRejectedとの二つの関数を引数とする
  then (onFulfilled, onRejected) {
    const { value, state } = this
    switch (state) {
      // ステートが変わった場合はその関数を実行
      case 'fulfilled':
        onFulfilled(value)
        break
      case 'rejected':
        onRejected(value)
        break
    }
  }
}

この段階のコードで実行してみると:

const p1 = new MyPromise((resolve, reject) => {
   resolve('成功')
   reject('失敗')
})

const p2 = new MyPromise((resolve, reject) => {
   reject('失敗')
   resolve('成功')
})

p1.then(val => {
  console.log('resolve', val)
}, val => {
  console.log('reject', val)
})
// resolve 成功

p2.then(val => {
  console.log('resolve', val)
}, val => {
  console.log('reject', val)
})
// reject 失敗

はい、一回ステート変更したら、もう一つの実行がされないので、どちらも一つしかログがありません。これで初歩的なプロミスができました。

非同期処理の問題

ここまでは全部同期コードでテストしていましたが、非同期のコードがあると、少し問題があります。例えば:

const p = new MyPromise((resolve, reject) => {
  setTimeout(()=> {
   resolve('成功')
  },1000)
})

p.then(val => {
  console.log('resolve', val)
}, val => {
  console.log('reject', val)
})

実行してみると、ログはありません。。理由も簡単ですが、先ほどのthenの実装では、ステート変更を前提にしていますが、非同期のコードがある場合、ステートがpendingのままがあり得るので、そのケースの対応がありませんでした。

早速thenを改修しますが、要するに、pending状態であっても、どこかでそのコールバック関数を保存しておくと、いざ状態が変わった場合に、保存先から取り出して実行すれば良い、とのことですね。ここで二つの変数を追加し、成功と失敗のコールバック関数を保存します。

class MyPromise {
  constructor(executor) {
    // ...

    this.onFulfilledCallback = undefined
    this.onRejectedCallback = undefined

    // ...
  }

  // then関数は、onFulfilledとonRejectedとの二つの関数を引数とする
  then (onFulfilled, onRejected) {
    const { value, state } = this
    switch (state) {
      // ステートが変わった場合はその関数を実行
      case 'fulfilled':
        onFulfilled(value)
        break
      case 'rejected':
        onRejected(value)
        break
      // ここはどの状態に変わるかが不明なのでとりあえず両方を保存します
      case 'pending':
        this.onFulfilledCallback = onFulfilled
        this.onRejectedCallback = onRejected
    }
  }
}

すると、rejectresolve関数では、保存されたコールバック関数を実行します:


  resolve(val) {
    if (this.state === 'pending') {
      this.state = 'fulfilled'
      this.value = val
      // 存在する場合にそれを実行
      this.onFulfilledCallback && this.onFulfilledCallback(val)
    }
  }

  reject(val) {
    if (this.state === 'pending') {
      this.state = 'rejected'
      this.value = val
      this.onRejectedCallback && this.onRejectedCallback(val)
    }
  }

この状態でもう一度先ほどの非同期処理を試してみると:

const p = new MyPromise((resolve, reject) => {
  setTimeout(()=> {
   resolve('成功')
  },1000)
})

p.then(val => {
  console.log('resolve', val)
}, val => {
  console.log('reject', val)
})
// 1秒後に resolve成功

thenを2回目呼び出す

これまでに非同期対応できましたが、一つ問題があります。thenの引数コールバックを保存するのは、一つの変数なので、もしthenを2回以上呼び出すと、2回目の引数が1回目のものを上書きしてしまいます。例えば:

const p = new MyPromise((resolve, reject) => {
  setTimeout(()=> {
   resolve('成功')
  },1000)
})

p.then((val)=>{
  console.log('1回目')
  console.log('resolve', val)
})

p.then((val)=>{
  console.log('2回目')
  console.log('resolve', val)
})

p.then((val)=>{
  console.log('3回目')
  console.log('resolve', val)
})

すると、最後の3回目しかログにでません。1回目が2回目の呼び出し時に上書きされ、2回目が3回目に上書きされているからです。

これを修復するために、コールバック関数を保存する変数を配列に変えます:

class MyPromise {
  constructor(executor) {
    // 関数以外の引数はエラーを出す
    if (!(executor instanceof Function)) {
      throw new Error('Require a function to init MyPromise')
    }
    // 初期ステートをpending、値をundefinedに
    this.state = 'pending'
    this.value = undefined
    this.onFulfilledQueue = []
    this.onRejectedQueue = []

    // executor関数を実行、ここでコンテキストのthisをプロミスインスタンスにバインドする
    try {
      executor(this.resolve.bind(this), this.reject.bind(this))
    } catch (e) {
      this.reject(e)
    }  
  }

  // then関数は、onFulfilledとonRejectedとの二つの関数を引数とする
  then (onFulfilled, onRejected) {
    const { value, state } = this
    switch (state) {
      // ステートが変わった場合はその関数を実行
      case 'fulfilled':
        onFulfilled(value)
        break
      case 'rejected':
        onRejected(value)
        break
      // ここは配列に追加
      case 'pending':
        this.onFulfilledQueue.push(onFulfilled)
        this.onRejectedQueue.push(onRejected)
        break
    }
  }

  resolve(val) {
    if (this.state === 'pending') {
      this.state = 'fulfilled'
      this.value = val
      // 配列に未実行の関数が存在する場合、先頭から取り出して実行
      while (this.onFulfilledQueue.length) {
        this.onFulfilledQueue.shift()(val)
      }
    }
  }

  reject(val) {
    if (this.state === 'pending') {
      this.state = 'rejected'
      this.value = val
      while (this.onRejectedQueue.length) {
        this.onRejectedQueue.shift()(val)
      }
    }
  }
}

この状態でもう一度テストしてみると:

thenのチェイニング機能

プロミスオブジェクトにthenメソッドがあり、そのthenを永遠にチェインしていくことができます。なんか不思議に見えるかもしれませんが、ORMのクエリビルダーとか、jsのmap/filterとかも事実上チェインしています。その「秘密」というと、自分自身(例えばORMの場合)、もしくはもう一つのインスタンス(例えばmapで新しい配列をリターン)しているから、同じメソッドで新しいインスタンスに対して実行することが可能になっています。

ただ、thenでプロミス自分自身をリターンすると、サイクルになってしまうのでエラーになります。

なので、ここのthenは、新しいプロミスのインスタンスをリターンすることで、チェインできるようにします:

  then (onFulfilled, onRejected) {
    const { value, state } = this
    // thenチェイニングできるように、新しいプロミスをリターン
    return new MyPromise((resolveNext, rejectNext) => {
      switch (state) {
        case 'fulfilled':
          onFulfilled(value)
          break
        case 'rejected':
          onRejected(value)
          break
        case 'pending':
          this.onFulfilledQueue.push(onFulfilled)
          this.onRejectedQueue.push(onRejected)
          break
      }
    })
  }

ただ、これだけでは足りません。今は新しいプロミスをリターンしているので、新しいプロミスの状態はどう変わるか、それを決めておく必要があります。現在のプロミスがrejectの場合、チェインが中止されてcatchまでいくので、一旦無視。pendingの場合はすでにコールバックの保存ができているので関心はしない。結局、現在のプロミスがもしresoveされたら、そのリターン値を、次のthenのコールバックの引数として渡すことになるので、fulfilledに変わった場合にその処理をする必要があります:

  then (onFulfilled, onRejected) {
    const { value, state } = this
    // thenチェイニングできるように、新しいプロミスをリターン
    return new MyPromise((resolveNext, rejectNext) => {
      switch (state) {
        case 'fulfilled':
          // ここでリターン値を取得し、次のプロミスのresolveコールバックに渡して実行
          const res = onFulfilled(value)
          resolveNext(res)
          break
        case 'rejected':
          onRejected(value)
          break
        case 'pending':
          this.onFulfilledQueue.push(onFulfilled)
          this.onRejectedQueue.push(onRejected)
          break
      }
    })
  }

ただただ、これでも問題があります。というのは、resが必ずしも一般の値というわけではなく、プロミスインスタンスの可能性もあります。もしresがプロミスの場合、そのプロミスのthenメソッドで状態変更をする必要があります。

  then (onFulfilled, onRejected) {
    const { value, state } = this
    // thenチェイニングできるように、新しいプロミスをリターン
    return new MyPromise((resolveNext, rejectNext) => {
      switch (state) {
        case 'fulfilled':
          // ここでリターン値を取得し、次のプロミスのresolveコールバックに渡して実行
          const res = onFulfilled(value)
          // プロミスの場合はthenで状態変更、一般値の場合はそのままresolve
          if (res instanceof MyPromise) {
            res.then(resolveNext, rejectNext)
          } else {
            resolveNext(res)
          }
          break
        case 'rejected':
          onRejected(value)
          break
        case 'pending':
          this.onFulfilledQueue.push(onFulfilled)
          this.onRejectedQueue.push(onRejected)
          break
      }
    })
  }

この状態で、thenをチェイニングしてみると:


const p = new MyPromise((resolve, reject) => {
  resolve('成功')
})

p.then(val=>{
  console.log('1回目', val)
  return '1回目リターン値'
}).then(val=>{
  console.log('2回目', val)
  return '2回目リターン値'
}).then(val=>{
  console.log('3回目', val)
}).then(val=>{
  console.log('4回目', val)
})

もうだいぶできましたね!この段階のコードは以下となります:

class MyPromise {
  constructor(executor) {
    // 関数以外の引数はエラーを出す
    if (!(executor instanceof Function)) {
      throw new Error('Require a function to init MyPromise')
    }
    // 初期ステートをpending、値をundefinedに
    this.state = 'pending'
    this.value = undefined
    this.onFulfilledQueue = []
    this.onRejectedQueue = []

    // executor関数を実行、ここでコンテキストのthisをプロミスインスタンスにバインドする
    try {
      executor(this.resolve.bind(this), this.reject.bind(this))
    } catch (e) {
      this.reject(e)
    }  
  }

  // then関数は、onFulfilledとonRejectedとの二つの関数を引数とする
  then (onFulfilled, onRejected) {
    const { value, state } = this
    return new MyPromise((resolveNext, rejectNext) => {
      switch (state) {
        case 'fulfilled':
          // ここでリターン値を取得し、次のプロミスのresolveコールバックに渡して実行
          const res = onFulfilled(value)
          // プロミスの場合はthenで状態変更、一般値の場合はそのままresolve
          if (res instanceof MyPromise) {
            res.then(resolveNext, rejectNext)
          } else {
            resolveNext(res)
          }
          break
        case 'rejected':
          onRejected(value)
          break
        case 'pending':
          this.onFulfilledQueue.push(onFulfilled)
          this.onRejectedQueue.push(onRejected)
          break
      }
    })
  }

  resolve(val) {
    if (this.state === 'pending') {
      this.state = 'fulfilled'
      this.value = val
      // 配列に未実行の関数が存在する場合、先頭から取り出して実行
      while (this.onFulfilledQueue.length) {
        this.onFulfilledQueue.shift()(val)
      }
    }
  }

  reject(val) {
    if (this.state === 'pending') {
      this.state = 'rejected'
      this.value = val
      while (this.onRejectedQueue.length) {
        this.onRejectedQueue.shift()(val)
      }
    }
  }
}

catchメソッド

これまでthenメソッドの実装が大体できました。次にエラーが出る時に実行するcatchメソッドの実装を考えます。

thenができたら、catchメソッドも簡単です。結構忘れられがちですが、thenは、成功時と失敗時のコールバックを引数としているので、事実上catchの仕事もこなす事が可能です。

const promise = new MyPromise((resolve, reject) => {
  throw new Error('エラーが起こりました')
})

// 二つ目のコールバックを入れると、エラーをキャッチしてくれます
promise.then(val => {
  console.log('成功', val)
}, err => {
  console.log(err.message)
})
// エラーが起こりました

ただ、使用中はいつも、thenに一つのコールバック関数しか渡しません。onRejectedまで書くと煩雑ですし、チェインしている中では見にくくなります。そのため、エラー捕獲の仕事は通常catchに任せています。なので、catchは簡単に言えば、onFulfilledがnullで、onRejectedだけを引数とするthenメソッドだと考えられます。

  // catchメソッドを追加
  catch (onRejected) {
    return this.then(null, onRejected)
  }

thenの引数タイプ判断

上記のcatch実装から、一つ新しい問題がもたらされました。thennullを引き渡すと、もちろん実行時にエラーとなるので、それを回避するために、エラー捕獲のロジックが必要となります。これは次の節で詳しく見てみたいのですが、その前に、本家のthenに、仮に関数以外の値を引数として渡しても、実はエラーが出ないのです。

const promise = new Promise((resolve, reject) => {
  resolve('成功')
})

promise
  .then(null)
  .then(5)
  .then('hoge')
  .then(value => console.log(value))
// 成功

つまり、関数以外の引数が引き渡されてもエラーを出さず、次のthenに値をパスしていくとのことです。

そのため、ここでthenメソッドに、引数のタイプについて判断を追加し、関数以外の場合は次のthenに持っていくように変えます:

then(onFulfilled, onRejected) {
  // 関数であればそのまま使う、出なければ値を渡す関数にする
  onFulfilled = onFulfilled instanceof Function ? onFulfilled : value => value
  // 関数以外の場合はエラーを出して値を渡す
  onRejected = onRejected instanceof Function ? onRejected : value => {throw value}
  // ...
}

エラー捕獲追加

thenに二つ目のコールバックがあれば、それが前のプロミスからのエラーを捕獲可能ですが、なければ次のonRejectedのあるthenか、catchまで、そのエラー情報を渡し続けることになります。

これを踏まえて、先ほどのthenの実装に、エラーが生じる場合の処理を追加します

  // then関数は、onFulfilledとonRejectedとの二つの関数を引数とする
  then (onFulfilled, onRejected) {
    onFulfilled = onFulfilled instanceof Function ? onFulfilled : value => value
    onRejected = onRejected instanceof Function ? onRejected : value => {throw value}

    const { value, state } = this
    return new MyPromise((resolveNext, rejectNext) => {
      switch (state) {
        case 'fulfilled':
          try {
            const res = onFulfilled(value)
            if (res instanceof MyPromise) {
              res.then(resolveNext, rejectNext)
            } else {
              resolveNext(res)
            }
          } catch (e) {
            // ここでエラー処理を追加
            rejectNext(e)
          }
          break
        case 'rejected':
          onRejected(value)
          break
        case 'pending':
          this.onFulfilledQueue.push(onFulfilled)
          this.onRejectedQueue.push(onRejected)
          break
      }
    })
  }

ただし、ここのエラー処理は、fulfilledのケースだけ実装しています。rejectedpendingのケースもtry/catchが必要なので、成功と失敗のエラー捕獲付きの処理をそれぞれ関数として抽出します。

  then (onFulfilled, onRejected) {
    onFulfilled = onFulfilled instanceof Function ? onFulfilled : value => value
    onRejected = onRejected instanceof Function ? onRejected : value => {throw value}

    const { value, state } = this
    return new MyPromise((resolveNext, rejectNext) => {
      //
      const onFulfilledFn = (value) => {
        try {
          const res = onFulfilled(value)
          if (res instanceof MyPromise) {
            res.then(resolveNext, rejectNext)
          } else {
            resolveNext(res)
          }
        } catch (e) {
          rejectNext(e)
        }
      }

      const onRejectedFn = (value) => {
        try {
          // ここだけ違います
          const res = onRejected(value)
          if (res instanceof MyPromise) {
            res.then(resolveNext, rejectNext)
          } else {
            resolveNext(res)
          }
        } catch (e) {
          rejectNext(e)
        }
      }
      switch (state) {
        case 'fulfilled':
          onFulfilledFn(value)
          break
        case 'rejected':
          onRejectedFn(value)
          break
        case 'pending':
          //ここはエラー処理付きのバージョンをプッシュ
          this.onFulfilledQueue.push(onFulfilledFn)
          this.onRejectedQueue.push(onRejectedFn)
          break
      }
    })
  }

二つの関数には共通の部分が多いなので、一部を抽出してさらに簡潔化します:

  then (onFulfilled, onRejected) {
    onFulfilled = onFulfilled instanceof Function ? onFulfilled : value => value
    onRejected = onRejected instanceof Function ? onRejected : value => {throw value}

    const { value, state } = this
    return new MyPromise((resolveNext, rejectNext) => {

      const onFulfilledFn = (value) => {
        try {
          const res = onFulfilled(value)
          this._resolvePromise(res, resolveNext, rejectNext)
        } catch (e) {
          rejectNext(e)
        }
      }

      const onRejectedFn = (value) => {
        try {
          const res = onRejected(value)
          this._resolvePromise(res, resolveNext, rejectNext)
        } catch (e) {
          rejectNext(e)
        }
      }

      switch (state) {
        case 'fulfilled':
          onFulfilledFn(value)
          break
        case 'rejected':
          onRejectedFn(value)
          break
        case 'pending':
          //ここはエラー処理付きのバージョンをプッシュ
          this.onFulfilledQueue.push(onFulfilledFn)
          this.onRejectedQueue.push(onRejectedFn)
          break
      }
    })
  }

  _resolvePromise(res, resolve, reject) {
    if (res instanceof MyPromise) {
      res.then(resolve, reject)
    } else {
      resolve(res)
    }
  }

この段階のコードは以下となります。

class MyPromise {
  constructor(executor) {
    // 関数以外の引数はエラーを出す
    if (!(executor instanceof Function)) {
      throw new Error('Require a function to init MyPromise')
    }
    // 初期ステートをpending、値をundefinedに
    this.state = 'pending'
    this.value = undefined
    this.onFulfilledQueue = []
    this.onRejectedQueue = []

    // executor関数を実行、ここでコンテキストのthisをプロミスインスタンスにバインドする
    try {
      executor(this.resolve.bind(this), this.reject.bind(this))
    } catch (e) {
      this.reject(e)
    }  
  }

  then (onFulfilled, onRejected) {
    onFulfilled = onFulfilled instanceof Function ? onFulfilled : value => value
    onRejected = onRejected instanceof Function ? onRejected : value => {throw value}

    const { value, state } = this
    return new MyPromise((resolveNext, rejectNext) => {

      const onFulfilledFn = (value) => {
        try {
          const res = onFulfilled(value)
          this._resolvePromise(res, resolveNext, rejectNext)
        } catch (e) {
          rejectNext(e)
        }
      }

      const onRejectedFn = (value) => {
        try {
          const res = onRejected(value)
          this._resolvePromise(res, resolveNext, rejectNext)
        } catch (e) {
          rejectNext(e)
        }
      }

      switch (state) {
        case 'fulfilled':
          onFulfilledFn(value)
          break
        case 'rejected':
          onRejectedFn(value)
          break
        case 'pending':
          //ここはエラー処理付きのバージョンをプッシュ
          this.onFulfilledQueue.push(onFulfilledFn)
          this.onRejectedQueue.push(onRejectedFn)
          break
      }
    })
  }

  catch (onRejected) {
    return this.then(null, onRejected)
  }

  _resolvePromise(res, resolve, reject) {
    if (res instanceof MyPromise) {
      res.then(resolve, reject)
    } else {
      resolve(res)
    }
  }

  resolve(val) {
    if (this.state === 'pending') {
      this.state = 'fulfilled'
      this.value = val
      // 配列に未実行の関数が存在する場合、先頭から取り出して実行
      while (this.onFulfilledQueue.length) {
        this.onFulfilledQueue.shift()(val)
      }
    }
  }

  reject(val) {
    if (this.state === 'pending') {
      this.state = 'rejected'
      this.value = val
      while (this.onRejectedQueue.length) {
        this.onRejectedQueue.shift()(val)
      }
    }
  }
}

このコードで前編の長い例で試してみると、本家のプロミスと同じ結果になるはずです:

console.log("create new promise");

let p = new MyPromise((resolve, reject) => {
  console.log("constructor callback");
  resolve("promise value");
});

p.then((res) => {
  console.log(res); // promise value
})
  .then((res) => {
    console.log(res); // undefined
    return 123;
  })
  .then((res) => {
    console.log(res); // 123
    return new MyPromise((resolve) => {
      resolve(456);
    });
  })
  .then((res) => {
    console.log(res); // 456
    return "return value";
  })
  .then()
  .then("string")
  .then((res) => {
    console.log(res); // return value
    throw new Error("error occurred!");
  })
  .then((res) => {
    console.log(res); // 出力なし
  })
  .catch((err) => {
    console.error(err.message); // error occurred!
    return "error!";
  })
  .then((res) => {
    console.log("after catch " + res); // after catch error!
    return 789;
  })
  // .finally((res) => {
  //   console.log(res); // undefined
  //   console.log("done!"); // done!
  // });

console.log("chain ends");

finallyはまだなのでまた後で追加します。

resolverejectのスタティックメソッド

やれやれ、ようやくコアな部分を実現できた気がしました。本家のプロミスには、Promise.resolve(1)とかで、スタティックのメソッドでプロミスインスタンスをリターンすることが出来ます。要するにプロミスのインスタンスをリターンすれば良いので、これを追加していきます。

class MyPromise {
  // ...
  static resolve(value) {
    return new MyPromise(resolve => { resolve(value) })
  }

  static reject(value) {
    return new MyPromise((resolve, reject) => { reject(value) })
  }
  //...
}

finallyメソッド

finallythenをベースに実装可能です。上記のスタティックメソッドを借りるのでここまで待ちました。

thencatchの違いといえば、プロミスの状態はどうでも良いとのことで、必ず実行するところにあります。ただ注意してほしいのは、finallyもプロミスをリターンします。つまりfinallyの後でもチェインは可能です。

  finally (callback) {
    return this.then(
      value  => MyPromise.resolve(callback()).then(() => value),
      err => MyPromise.resolve(callback()).then(() => { throw err })
    )
  }

allrace

最後に、allraceのスタティックメソッドも追加します。これらについて前編では簡単に紹介していますが、詳しくはMDNで。

23/04/23更新:empty arrayのケース対応されていない問題修復

  static all(promises) {
    return new MyPromise((resolve, reject) => {
      let results = []
      let count = 0
      if (promises.length === 0) {
        resolve(results)
        return
      }
      for (let [idx, promise] of promises.entries()) {
        // もしプロミスインスタンスではない場合、一回プロミスに変換する
        if (!(promise instanceof MyPromise)) {
          promise = MyPromise.resolve(promise)
        }
        // 次にthenを呼び出す
        promise.then(res => {
          // 結果はプロミスの投入順番と対応するためindex必須
          results[idx] = res
          count++
          // すべてのプロミスがfulfilledになれば、結果配列をリターン
          if (count === promises.length) {
            resolve(results)
          }
        }, err => {
          // どれか一つのプロミスが失敗したら、全体も失敗
          reject(err)
        })
      }
    })
  }

allSettledallはほぼ一緒ですが、要するに要素の結果はどうであれ、必ずすべての要素が状態変更するまで待つ、とのことなので、ここは省略します。

次にraceを実装します。どれかが状態変更したら、全体的に状態変更となりますので、割とわかりやすいかもしれません。

  static race(promises) {
    return new MyPromise((resolve, reject) => {
      for (let [idx, promise] of promises.entries()) {
        // もしプロミスインスタンスではない場合、一回プロミスに変換する
        if (!(promise instanceof MyPromise)) {
          promise = MyPromise.resolve(promise)
        }
        // 次にthenを呼び出す
        promise.then(res => {
          resolve(res)
        }, err => {
          reject(err)
        })
      }
    })
  }

また、ポイントフリーな書き方にすると、よりシンプルになります。

  static race(promises) {
    return new MyPromise((resolve, reject) => {
      promises.forEach(p => MyPromise.resolve(p).then(resolve, reject))
    })
  }

最終的なコード

こちら:

class MyPromise {
  constructor(executor) {
    // 関数以外の引数はエラーを出す
    if (!(executor instanceof Function)) {
      throw new Error("Require a function to init MyPromise");
    }
    // 初期ステートをpending、値をundefinedに
    this.state = "pending";
    this.value = undefined;
    this.onFulfilledQueue = [];
    this.onRejectedQueue = [];

    // executor関数を実行、ここでコンテキストのthisをプロミスインスタンスにバインドする
    try {
      executor(this.resolve.bind(this), this.reject.bind(this));
    } catch (e) {
      this.reject(e);
    }
  }

  then(onFulfilled, onRejected) {
    onFulfilled =
      onFulfilled instanceof Function ? onFulfilled : (value) => value;
    onRejected =
      onRejected instanceof Function
        ? onRejected
        : (value) => {
            throw value;
          };

    const { value, state } = this;
    return new MyPromise((resolveNext, rejectNext) => {
      const onFulfilledFn = () => {
        try {
          const res = onFulfilled(value);
          this._resolvePromise(res, resolveNext, rejectNext);
        } catch (e) {
          rejectNext(e);
        }
      };

      const onRejectedFn = () => {
        try {
          const res = onRejected(value);
          this._resolvePromise(res, resolveNext, rejectNext);
        } catch (e) {
          rejectNext(e);
        }
      };

      switch (state) {
        case "fulfilled":
          onFulfilledFn(value);
          break;
        case "rejected":
          onRejectedFn(value);
          break;
        case "pending":
          this.onFulfilledQueue.push(onFulfilledFn);
          this.onRejectedQueue.push(onRejectedFn);
          break;
      }
    });
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }

  finally(callback) {
    return this.then(
      (value) => MyPromise.resolve(callback()).then(() => value),
      (err) =>
        MyPromise.resolve(callback()).then(() => {
          throw err;
        })
    );
  }

  _resolvePromise(res, resolve, reject) {
    if (res instanceof MyPromise) {
      res.then(resolve, reject);
    } else {
      resolve(res);
    }
  }

  resolve(val) {
    if (this.state === "pending") {
      this.state = "fulfilled";
      this.value = val;
      while (this.onFulfilledQueue.length) {
        this.onFulfilledQueue.shift()(val);
      }
    }
  }

  reject(val) {
    if (this.state === "pending") {
      this.state = "rejected";
      this.value = val;
      while (this.onRejectedQueue.length) {
        this.onRejectedQueue.shift()(val);
      }
    }
  }

  static resolve(value) {
    return new MyPromise((resolve) => {
      resolve(value);
    });
  }

  static reject(value) {
    return new MyPromise((resolve, reject) => {
      reject(value);
    });
  }

  static race(promises) {
    return new MyPromise((resolve, reject) => {
      for (let [idx, promise] of promises.entries()) {
        if (!(promise instanceof MyPromise)) {
          promise = MyPromise.resolve(promise);
        }
        // 次にthenを呼び出す
        promise.then(
          (res) => {
            resolve(res);
          },
          (err) => {
            reject(err);
          }
        );
      }
    });
  }

  static all(promises) {
    return new MyPromise((resolve, reject) => {
      let results = [];
      let count = 0;
      if (promises.length === 0) {
        resolve(results);
        return;
      }
      for (let [idx, promise] of promises.entries()) {
        if (!(promise instanceof MyPromise)) {
          promise = MyPromise.resolve(promise);
        }
        // 次にthenを呼び出す
        promise.then(
          (res) => {
            results[idx] = res;
            count++;
            if (count === promises.length) {
              resolve(results);
            }
          },
          (err) => {
            reject(err);
          }
        );
      }
    });
  }
}

終わりに

やれやれ、ようやく終わりました。。プロミスについてこの内容を基礎編に入れるのがちょっと厳しい気がしますね。やっている中でなかなか理解に苦しむところが多かったのです。

自分の理解もまだまだなところがあると思いますので、随時修正するかもしれません。もし何か問題があれば、ご指摘いただけれ大変ありがたいです。。

ではでは、良いコーディングライフを。

Discussion