背景
JSを始めると必ずぶち当たる「Promiseの壁」
ググってもよく分からなかったので自分なりにまとめてみようと思った
こんな人向け
- 最近JS始めたよ
- とりあえず
await
付けとけってじっちゃが言ってた -
Promise
何となく使ってるけど、いつ事故を起こすかビクビクしている Promise完全に理解した
- 茶番が好き
上記は全て著者のことですが、一旦全て棚に上げさせてください(そうしないと記事が自虐だけで終わってしまいます)
注意
当記事は8割が茶番で構成されております。
真面目な記事をお探しの方は即座にブラウザバックされることを推奨いたします。
本編
1. そもそも非同期処理って?
相手と仲良くなるには、まず相手のことを知ることが重要です。まずは基本から。
JavaScript Primer
より引用
非同期処理はコードを順番に処理していきますが、ひとつの非同期処理が終わるのを待たずに次の処理を評価します。 つまり、非同期処理では同時に実行している処理が複数あります。
[同期処理] [非同期処理]
処理A[2s] 処理A[2s]
⬇︎ ⬇︎
処理B[1s] 処理B[1s]
[結果] [結果]
処理A出力 処理B出力
処理B出力 処理A出力
計3[s] 計2[s]
まぁ、書いてある通りですね。(説明するとこなかった)
JavaScriptではその非同期処理をPromise
という形で扱ってるってことですね。
2. 教えて!Promiseくん
次にPromiseくんについて詳しくみていきます。転校生への質問責めのごとく、根掘り葉掘り聞いていきましょう。(リアルでやると引かれます)
何ができるの?(ぶしつけ)
const p = new Promise(() => {
console.log('Promise!!!');
});
p;
// "Promise!!!"
どうやら、new Promise()
と書いてあげると非同期処理が作れるみたいです。
スゴイネ。
試しにそれっぽいことやってみせてよ(無茶振り)
const p = new Promise(() => {
setTimeout(() => {
console.log('Promise!!!')
}, 1000);
});
p;
console.log('同期処理');
// "同期処理"
// "Promise!!!"
SetTimeout()
は第1引数に渡した処理を第2引数[ms]後に実行する関数です。
それを非同期処理にしているので、後から実行した同期処理が先に出力されていますね。
おぉ、何だかそれっぽい!
(次の話題は何にしようか・・・、あ!)
もしかしてそれ"HIDOKIES"のキーホルダー?欲しいな〜(強欲)
const p = new Promise((resolve) => {
setTimeout(() => {
console.log('1s経過');
resolve('o---o[HIDOKIES]');
}, 1000);
});
console.log('👋', p);
// "👋" [object Promise]{}
// "1秒経過"
放物線を描き、1秒後にHIDOKIESのキーホルダーが手に納まる━━━━━
ハズが、Promiseくんの手を離れた瞬間に[[object Promise]{}]
になって、手の中に瞬間移動していました。
え、ナニコレ、こっわ!!Promniseくんってマジシャン!?
てか、なにこの塊。キーホルダーの原型ないじゃん・・・
調子に乗ってすみませんでした。タネも仕掛けも教えてください・・・
const p = new Promise((resolve) => {
setTimeout(() => {
console.log('1s経過');
resolve('o---o[HIDOKIES]');
}, 1000);
});
// then()で構えたら受け取れるよ
p.then(v => {
console.log('👋', v);
});
// 1s経過
// "👋" "o---o[HIDOKIES]"
Promiseくんの言う通りに手を構えるとあら不思議。1秒後にちゃんとキーホルダーが受け取れました。
彼云く、「PromiseはPromiseオブジェクトを返すから、thenで結果を受け取ってあげる必要がある」とのこと。「分かっている人の話ほど分からない理論」ですね
わかるように説明してよ(怒)
[mozilla-Promise] (https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise)より引用
Promise インターフェイスは作成時点では分からなくてもよい値へのプロキシです。 Promise を用いることで、非同期アクションの成功や失敗に対するハンドラーを関連付けることができます。これにより、非同期メソッドは、最終的な値を返すのではなく、未来のある時点で値を持つ Promise を返すことで、同期メソッドと同じように値を返すことができるようになります。
要は、Promise自体の戻り値は結果が未定のまま即座にPromiseオブジェクトという形で返されますよ。ってことです。
さっきの[object Promise]{}
はPromiseオブジェクトそのもので、当然この時点では結果が未定なわけです。
では、未来に結果が確定したときどうやって受け取るのでしょう?
続けて引用
Promise の状態は以下のいずれかとなります。
- 待機pending: 初期状態。成功も失敗もしていません。
- 満足fulfilled: 処理が成功して完了したことを意味します。
- 拒絶rejected: 処理が失敗したことを意味します。
待機状態のプロミスは、何らかの値を持つ満足 (fulfilled) 状態、もしくは何らかの理由 (エラー) を持つ拒絶 (rejected) 状態のいずれかに変わります。
うわ・・・難しそうな単語の羅列・・・。難しそうな単語アレルギーが出そうです。
先ほどPromiseくんがキーホルダーを投げる時に、resolve()
を使っていたのを覚えていますでしょうか?(伏線張るの下手くそかよ)
これは、Promiseオブジェクトを「満足」状態に移行させる、つまり、ここで結果が確定するわけですね。
reject()
なるものが対になって存在していますが、「拒絶」状態にするだけで結果を確定させることに違いはありません。
┌> resolve() -> 満足(成功)
待機 ┤
└> reject() -> 拒絶(失敗)
うーん、なんとなく分かった気がする・・・
いいなぁ〜HIDOKIESのキーホルダー、私たちにもちょうだ〜い!(便乗)
const keyholderList = ['o---o[HIDOKIES1]', 'o---o[HIDOKIES2]', 'o---o[HIDOKIES3]'];
// 関数化することで呼ばれるたびに新しいPromiseを生成します
// 一度「結果が確定」したPromiseは何度呼び出しても結果は「確定したまま」です
const throwKeyholder = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const keyholder = keyholderList.pop();
if(keyholder) resolve(keyholder);
else reject('ごめんね。もうキーホルダーなくなっちゃった。');
}, 1000);
});
};
throwKeyholder().then(v => {
console.log('👋A', v);
});
throwKeyholder().then(v => {
console.log('👋B', v);
});
throwKeyholder().then(v => {
console.log('👋C', v);
});
throwKeyholder().then(v => {
console.log('👋D', v);
});
// "👋A" "o---o[HIDOKIES3]"
// "👋B" "o---o[HIDOKIES2]"
// "👋C" "o---o[HIDOKIES1]"
A、B、Cの3人が喜んでいる中、Dが怒り出しました。「どうして私のこと無視するのよ!」
それに対して、Promiseくんは「ちゃんとキーホルダーはもうないよって言ったよ?」の一点張り。
さて、悪いのはどちらでしょうか?
正解はDです。なぜなら、Promiseくんはちゃんとreject()
でキーホルダーがない旨をあやまっているからです。
どうしてDさんは聞き取れなかったの?
const keyholderList = ['o---o[HIDOKIES1]', 'o---o[HIDOKIES2]', 'o---o[HIDOKIES3]'];
const throwKeyholder = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const keyholder = keyholderList.pop();
if(keyholder) resolve(keyholder);
else reject('ごめんね。もうキーホルダーなくなっちゃった。');
}, 1000);
});
};
throwKeyholder().then(v => {
console.log('👋A', v);
});
throwKeyholder().then(v => {
console.log('👋B', v);
});
throwKeyholder().then(v => {
console.log('👋C', v);
});
throwKeyholder().then(v => {
console.log('👋D', v);
}, e => {
console.log('👂D', e);
});
// "👋A" "o---o[HIDOKIES3]"
// "👋B" "o---o[HIDOKIES2]"
// "👋C" "o---o[HIDOKIES1]"
// "👂D" "ごめんね。もうキーホルダーなくなっちゃった。"
今度はちゃんと「聞く耳」を持っていたので聞こえましたね!めでたしめでたし。
then()
はreject()
された時の動作も定義できるんです。
ただ、reject()
された時の動作はcatch()
で定義するほうが一般的です。
const p = new Promise((resolve, reject) => {
reject('rejectされたよ');
});
p.then(v => {
console.log("v", v);
}).catch(e => {
console.log("e", e);
});
// "e", "rejectされたよ"
3. Promiseくんってawaitさんってまさか・・・?
なんだかんだでクラスに馴染めた(?)Promiseくんでしたが、「async
教室でawait
さんといつも一緒にいるし、付き合っているのでは?」という噂が広まっていました。気になったので、本人に直接聞いてみることに。
awaitさんといつも一緒にいるけどなんで?
const p = new Promise((resolve) => {
setTimeout(() => {
console.log('1s経過');
resolve('Promise!!!');
}, 1000);
});
// awaitはasync付き関数内でしか使えません
const f = async() => {
await p.then(v => {
console.log("v", v);
});
console.log('同期処理');
};
f();
// "1秒経過"
// "v" "Promise!!!"
// "同期処理"
非同期処理のはずがawait
のおかげで結果が確定するまで待ってくれているようです。
非同期処理であるPromiseくんが同期処理になれるとはこういうことだったんですね。
えっと・・・それだけ?
const p = new Promise((resolve) => {
setTimeout(() => {
console.log('1s経過');
resolve('Promise!!!');
}, 1000);
});
const f = async() => {
console.log(await p); // 注目
console.log('同期処理');
};
f();
// "1秒経過"
// "Promise!!!"
// "同期処理"
await
さんと一緒の時は、then()
がなくても結果が受け取れています。
いちいちthen()
を書く手間が省けます。これはハッピーですね。
あれ?じゃあ、rejectされたときはどうすんのさ
const p = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('1s経過');
reject('rejectしたよ');
}, 1000);
});
const f = async() => {
await p.catch(e => {
console.log('e', e);
});
console.log('同期処理');
};
f();
// "1秒経過"
// "e" "rejectしたよ"
// "同期処理"
reject
を受け取る時はawait
さんが居ても居なくても同じみたいです。
resolve
された値のみが戻り値として受け取れるんですね。
そういえばasync教室でしか一緒にいないみたいだけどなんで?
え、そんだけ!?理由とかないの?解説記事としてどうなのそれ
色々調べてみましたがよくわからず・・・。見つかった文章は以下
awaitは非同期関数内(async付き関数)でしか使えません
じゃあさ、そもそもasyncってなんなの?
const p = new Promise((resolve) => {
setTimeout(() => {
console.log('1s経過');
resolve('Promise!!!');
}, 1000);
});
const f = async() => {
console.log(await p);
console.log('同期処理1');
};
f();
console.log('同期処理2');
// "同期処理2" <-- 注目
// "1秒経過"
// "Promise!!!"
// "同期処理1"
Promiseくんって実は痛い人なんじゃ・・・
あれ?「同期処理2」が先に実行されている!?
そうです。実はasync
を付けた関数は非同期処理になる、つまりはPromiseくんと同じというわけです。
ただの痛い人ではなかったんですね〜(痛くないとは言っていない)
もしかして、new Promise()
をしなくても非同期処理は作れるの?
const p = async() => {
return 'Promise!!!';
};
p().then(v => {
console.log('v', v);
});
// "v" "Promise!!!"
一つ質問いいかな、resolve()
どこ行った?
君のような勘のいいガキは嫌いだよ(条件反射)
new Promise()
の時はresolve()
で「満足」状態に移行していましたが、async
になった途端return
に入れ変わっています。
「入れ替わっている」というのは少し違うかも知れません。というのも、async
付きとはいえ関数ですからreturn
で値を返すのは当然ですね?
async
関数の戻り値はPromiseオブジェクト
ですので、new Promise()
で書き直すと以下になります。
const p = () => {
return new Promise((resolve) => {
resolve('Promise!!!');
});
};
p().then(v => {
console.log('v', v);
});
// "v" "Promise!!!"
同じことしてるハズなのにnew Promise()
の方が長いじゃん
上手にasync
を使うことで、コードを短く書けるってことですね。
個人的には、 なるべくasync
を選ぶことをお勧めします。関数として切り出す方が扱いやすいですし、中でawait
が使えてネストが減らせますしね。
そうはいってもrejectできないんじゃね?
const p = async() => {
throw('reject');
};
p().catch(e => {
console.log(e);
});
// "reject"
throw()
は「例外」、簡単に言うとエラーを返す関数です。これによって「拒絶」状態に移行しているわけです。
結局awaitさんと付き合ってるの?
const wait = (ms) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
};
const p = async(c, ms) => {
await wait(ms);
console.log(c);
};
Promise.all([
p('imagination', 8), p('will', 2),
p('up', 5), p('it', 4), p('I', 1),
p('your', 7), p('leave', 3), p('to', 6)
]);