JS基礎いろいろーPromise後編
前編では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
で、もちろん、resolve
とreject
メソッドがあります。
この状態で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
メソッドには、onFulfilled
とonRejected
の二つの関数を引数とし、現在のプロミスの状態が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
}
}
}
すると、reject
とresolve
関数では、保存されたコールバック関数を実行します:
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
実装から、一つ新しい問題がもたらされました。then
にnull
を引き渡すと、もちろん実行時にエラーとなるので、それを回避するために、エラー捕獲のロジックが必要となります。これは次の節で詳しく見てみたいのですが、その前に、本家の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
のケースだけ実装しています。rejected
とpending
のケースも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
はまだなのでまた後で追加します。
resolve
とreject
のスタティックメソッド
やれやれ、ようやくコアな部分を実現できた気がしました。本家のプロミスには、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
メソッド
finally
もthen
をベースに実装可能です。上記のスタティックメソッドを借りるのでここまで待ちました。
then
とcatch
の違いといえば、プロミスの状態はどうでも良いとのことで、必ず実行するところにあります。ただ注意してほしいのは、finally
もプロミスをリターンします。つまりfinally
の後でもチェインは可能です。
finally (callback) {
return this.then(
value => MyPromise.resolve(callback()).then(() => value),
err => MyPromise.resolve(callback()).then(() => { throw err })
)
}
all
とrace
最後に、all
とrace
のスタティックメソッドも追加します。これらについて前編では簡単に紹介していますが、詳しくは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)
})
}
})
}
allSettled
とall
はほぼ一緒ですが、要するに要素の結果はどうであれ、必ずすべての要素が状態変更するまで待つ、とのことなので、ここは省略します。
次に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