スペシャルサンクス
とある休日
娘(4歳)「ねえパパ」
ワイ「なんや、娘ちゃん?」
娘「非同期って何?」
ワイ「ひ、非道鬼!?」
娘「そうそう、非同期処理とかいうやつ」
ワイ「非道鬼を処理やて・・・!?」
非道鬼「ヴォ〜〜〜!!!」
娘「!?」
娘「・・・現れたわね、非道鬼」
娘「処理してあげる」
ワイ「娘ちゃん、まだ4歳なのに、もう厨二病か・・・?」
よめ太郎「おい」
よめ太郎「お前まさか、非同期も知らんのか・・・?」
ワイ「いやいや、まさかまさか」
ワイ「流石に知っとるわ」
ワイ「それはそれは・・・極悪非道な・・・鬼のことや・・・」
よめ太郎「お前が非道鬼に喰われてしまえ」
非同期処理とは
よめ太郎「ええか、娘ちゃん」
よめ太郎「まず、同期って言葉は」
よめ太郎「タイミングが合うって意味や」
娘「じゃあ、非同期っていうのはタイミングが合わないってこと?」
よめ太郎「せや」
娘「なんかピンとこないね・・・」
よめ太郎「ほな、例を挙げて説明するわ」
例えば、カウントダウンする処理
よめ太郎「ほな、例として」
よめ太郎「5、4、3、2、1、0
って感じで」
よめ太郎「カウントダウンする処理を書いてみるで」
娘「うん」
よめ太郎「まず↓こんな感じのコードを書いてみたで」
setTimeout(() => console.log(5), 1000);
setTimeout(() => console.log(4), 1000);
setTimeout(() => console.log(3), 1000);
setTimeout(() => console.log(2), 1000);
setTimeout(() => console.log(1), 1000);
setTimeout(() => console.log(0), 1000);
よめ太郎「↑このコードを実行するのに、合計で何秒かかると思う?」
娘「ええと、setTimeout()は確か」
娘「タイマーをセットするメソッドだから」
娘「1,000ミリ秒後・・・つまり1秒後にconsole.log(5)
が実行されるよね」
よめ太郎「せやな」
娘「その後、次の行が処理されるから」
娘「また1秒経過して、今度はconsole.log(4)
が実行される」
娘「5、4、3、2、1、0・・・だから」
娘「合計6秒かかる!」
よめ太郎「・・・って思うやろ?」
よめ太郎「実は1秒しか掛からへんねん」
娘「そうなんだ・・・!」
よめ太郎「1行目のsetTimeout()
に渡した関数は」
よめ太郎「1秒後に実行される訳やけど」
よめ太郎「その1秒間を待たずに、次の行が実行されてしまうねん」
娘「へえ〜」
よめ太郎「それが非同期処理やな」
娘「つまり・・・」
娘「1行目の処理が終わったら、2行目」
娘「2行目の処理が終わったら、3行目」
娘「そんな風にちゃんとタイミングを合わせて処理してくれるのが」
娘「同期的な処理で」
よめ太郎「そうそう」
娘「前の処理の完了を待たずに、次の処理が走るのが」
娘「非同期処理なんだね」
よめ太郎「その通りや」
よめ太郎「そして、さっきのsetTimeout()
は」
よめ太郎「非同期的に処理されるメソッド、ってことやな」
娘「なるほどね〜」
よめ太郎「ほかにも、外部APIからデータを取得する処理なんかも非同期やな」
よめ太郎「例えばQiitaのAPIからデータを取得しようとして・・・」
- QiitaのAPIから、やめ太郎のフォロワー一覧を取得して、変数に格納する。
- フォロワーさん達を、リストとして画面に表示する。
よめ太郎「↑こんな処理をしようとした場合に」
よめ太郎「非同期処理のことを考えずにコードを書いてしまうとマズいんや」
JSくん「よっしゃ、QiitaのAPIに通信開始や!」
JSくん「そして、すぐさまフォロワーさんの一覧を表示や!」
JSくん「このfollowers
いう変数に入っとるはずやな!」
JSくん「あれ?何も入っとらんで!」
JSくん「あかん、エラーや!!!」よめ太郎「そらせやろ」
よめ太郎「まだ通信中や!」
よめ太郎「↑こんな感じになってしまうんや」
娘「なるほどー」
娘「APIとの通信とかって、何ミリ秒かかるのか予測できないから」
娘「完了を待たずに次の処理をしてくれようとしちゃうんだね」
よめ太郎「せやな」
よめ太郎「setTimeout()
もそんな感じで」
よめ太郎「1秒待ってる間にも、次のコードを実行しようとしてしまうんや」
娘「同期が取れてないんだねぇ」
じゃあ、どうやってカウントダウンするの?
娘「でもさあ」
娘「それなら、どうやってカウントダウン機能を実装するの?」
ワイ「そこはワイに任しとき!」
ワイ「要はタイミングが合うように書いてやればええんや」
setTimeout(() => {
console.log(5);
// ここに次の処理を書く。
}, 1000);
ワイ「↑こんな感じや」
娘「どういうこと?」
ワイ「ええとな」
ワイ「setTimeout()
の第一引数には、1秒後に実行したい関数を渡すやろ?」
娘「うん」
ワイ「引数として渡される関数・・・つまりコールバック関数やな」
ワイ「そのコールバック関数の中に、次にやりたい処理も書いてやるんや」
ワイ「上のコードで言うと、console.log(5);
が実行された直後の部分に」
ワイ「次の処理も追加で書いてやればええんや」
娘「なるほどね」
娘「1秒経って、console.log(5);
が実行された後で」
娘「次の処理が走るように、ってことかぁ」
娘「じゃあ、そこに次のsetTimeout()
を書けばいいんだね!」
ワイ「そうそう」
ワイ「せやから次は・・・」
setTimeout(() => {
console.log(5);
setTimeout(() => {
console.log(4);
// ここに、更に次の処理を書く。
}, 1000);
}, 1000);
ワイ「↑こうやな」
ワイ「こんな感じで、どんどん入れ子にしてやればええねん」
娘「なるほどー」
娘「パパ、すご〜い!」
ワイ「最終的には↓こうや!」
setTimeout(() => {
console.log(5);
setTimeout(() => {
console.log(4);
setTimeout(() => {
console.log(3);
setTimeout(() => {
console.log(2);
setTimeout(() => {
console.log(1);
setTimeout(() => {
console.log(0);
}, 1000);
}, 1000);
}, 1000);
}, 1000);
}, 1000);
}, 1000);
娘「あれ?なんか・・・」
娘「地獄みたいに読みづらいコードになっちゃったけど・・・」
よめ太郎「コールバック地獄いうやつやな」
娘「このコールバック地獄に非道鬼が住んでるの?」
ワイ「せや」
よめ太郎「ムチャクチャ言うなや」
よめ太郎「せっかく娘ちゃんが理解しかけてたのに」
よめ太郎「非道鬼の話に戻ってしまったやないか」
ワイ「いやいや、綺麗なコードやと思うで」
ワイ「見てみい」
ワイ「天使が持ってるハープみたいやで」
ワイ「地獄どころか、天国や」
ワイ「ワイは好きやけどな〜」
よめ太郎「そんなに好きなら貴様を天国に送ったるわ」
ワイ「ひ、非道鬼ィ!」
よめ太郎「誰が非道鬼や」
娘「(なんだこの家・・・)」
それじゃあ、どうやって書けばいいの?
娘「で、結局どうすればいいの?」
娘「コールバック地獄にならないように、うまく非同期処理を繋げられる方法があるの?」
よめ太郎「あるで」
よめ太郎「Promiseとかasync/awaitやな」
娘「ふーん」
娘「パパ、Promiseやasync/awaitって何?」
ワイ「(いや何でワイに聞くねん)」
Promise、async/awaitとは
ワイ「プロミス・・・つまりやな」
ワイ「消費者金融にお金を借りに行く前に」
ワイ「a think」
ワイ「いったん考えるんや」
ワイ「気軽に何十万も借りると、後から大変なことになってまうからな」
娘「そっか」
ワイ「そして、await」
ワイ「審査の間は大人しく待つんや」
よめ太郎「結局申し込んどるやないかい」
よめ太郎「代われ、わしが説明する」
よめ太郎「まずはPromiseからや」
Promiseとは
よめ太郎「Promiseを使うと、連続した非同期処理を書くときにも」
よめ太郎「ネスト地獄にならずにフラットに書けるんや」
娘「へぇ〜」
娘「どんな風に使うの?」
よめ太郎「new Promise()
って感じで」
よめ太郎「promiseオブジェクトを生成するんや」
娘「promiseオブジェクト・・・」
よめ太郎「せや」
娘「よく分かんないけど、setTimeout()
はどこに書くの?」
よめ太郎「new Promise()
するときに、引数としてコールバック関数を渡すんやけど」
よめ太郎「そのコールバック関数の中にsetTimeout()
とかを書くんや」
娘「またコールバック・・・?」
娘「連続して処理すると地獄になっちゃわない・・・?」
よめ太郎「大丈夫や」
よめ太郎「やってみるで」
よめ太郎「new Promise()
するときにコールバック関数を渡すから・・・」
const promiseObj = new Promise(() => {});
よめ太郎「↑こんなイメージやな」
よめ太郎「こんな感じでpromiseオブジェクトを生成して、変数に格納するイメージや」
娘「うん」
よめ太郎「ほんで、そのコールバック関数の中に」
よめ太郎「setTimeout()
とかconsole.log(5)
を書くわけやから・・・」
const promiseObj = new Promise(resolve => {
setTimeout(() => {
console.log(5);
resolve();
}, 1000);
});
よめ太郎「↑こんな感じや」
よめ太郎「この時点でsetTimeout()
は実行されて」
よめ太郎「1秒タイマーが開始されるんや」
娘「じゃあ、1秒後にconsole.log(5)
が実行されるんだね」
娘「コールバック地獄のときは、そのconsole.log(5)
の後に」
娘「次のsetTimeout()
を書いてたけど」
娘「Promiseの場合は、次のsetTimeout
の代わりに」
娘「resolve()
っていうのを実行してるね」
娘「resolve()
を実行すると何が起こるの?」
よめ太郎「次に実行したい処理が実行されるんや」
娘「でもまだ次にやりたい処理は書いてないよ?」
娘「次のsetTimeout()
はどこに書くの?」
よめ太郎「次に処理したい内容は」
よめ太郎「promiseオブジェクトが持ってるthen()
メソッドに渡して登録するんや」
よめ太郎「コールバック関数としてな」
const promiseObj = new Promise(resolve => {
setTimeout(() => {
console.log(5);
resolve();
}, 1000);
});
// 次にやりたい処理はthen()に渡す。
promiseObj.then(() => {
setTimeout(() => {
console.log(4);
}, 1000);
});
よめ太郎「↑こんな感じや」
娘「へえ〜」
娘「setTimeout()
の中にsetTimeout()
・・・っていう風にネストしないで」
娘「外側に出てから次の処理を書けるんだね」
よめ太郎「せや」
よめ太郎「これはresolve()
君のおかげや」
よめ太郎「resolve()
を実行すると」
よめ太郎「then()
メソッドで登録した次の処理が実行されるんや」
娘「後からthen()
に渡したコールバック関数が」
娘「そのままresolve
になるんだね!」
よめ太郎「いや、then()
に渡した関数がそのままresolve
になる訳ではないんや」
よめ太郎「resolve
が実行されたら、then()
で登録した関数も実行される、っていうだけや」
娘「へー」
娘「下のthen()
で登録した関数が」
娘「上のresolve()
きっかけで発動する、ってことか」
娘「そのままresolve
に入って来る訳ではないんだね」
よめ太郎「せや」
よめ太郎「だから、何回もthen()
して、複数の関数を登録することもできるで?」
const promiseObj = new Promise(resolve => {
setTimeout(() => {
console.log(5);
resolve();
}, 1000);
});
promiseObj.then(() => {
setTimeout(() => {
console.log(4);
}, 1000);
});
// 複数回then()することもできる。
promiseObj.then(() => {
setTimeout(() => {
console.log('もう1個!');
}, 1000);
});
よめ太郎「↑こうやな」
よめ太郎「これで、1秒後に5
がコンソールに表示されて」
よめ太郎「それから更に1秒後に4
ともう1個!
がコンソールに表示されるんや」
娘「へえ〜」
娘「何となくだけど分かってきたかも」
娘「1つ目のsetTimeout()
の中に、2つ目のsetTimeout()
を書くんじゃなく」
娘「1つ外に出て、then()
に渡す関数のところで書けるから」
娘「連続でsetTimeout()
をしても、ネスト地獄にならないってことだね!」
よめ太郎「そう!」
よめ太郎「その通りや!」
よめ太郎「試しに、もっと連続してsetTimeout()
してみるで!」
よめ太郎「ちょっとコード長くてごめんやで」
new Promise(resolve => {
setTimeout(() => {
console.log(5);
resolve();
}, 1000);
})
.then(() => {
return new Promise(resolve => {
setTimeout(() => {
console.log(4);
resolve();
}, 1000);
});
})
.then(() => {
return new Promise(resolve => {
setTimeout(() => {
console.log(3);
resolve();
}, 1000);
});
})
.then(() => {
return new Promise(resolve => {
setTimeout(() => {
console.log(2);
resolve();
}, 1000);
});
})
.then(() => {
return new Promise(resolve => {
setTimeout(() => {
console.log(1);
resolve();
}, 1000);
});
})
.then(() => {
setTimeout(() => {
console.log(0);
}, 1000);
});
娘「ほんとだ、ネストはしてないね・・・」
娘「でも・・・」
もっとスッキリ書きたい
娘「もっとスッキリ書けないの?」
よめ太郎「書けるで」
よめ太郎「new Promise()
する部分を関数にしてやるんや」
よめ太郎「ほぼ同じこと何回も書いてるからな」
const promiseMaker = num => {
return new Promise(resolve => {
setTimeout(() => {
console.log(num);
resolve();
}, 1000);
});
};
よめ太郎「↑こんな感じや」
よめ太郎「console.log()
に渡す数値は毎回変わるから」
よめ太郎「このpromiseMaker()
の引数として渡せるようにしておいたで」
よめ太郎「引数名はnum
や」
よめ太郎「使い方としては・・・」
promiseMaker(5)
.then(() => promiseMaker(4))
.then(() => promiseMaker(3))
.then(() => promiseMaker(2))
.then(() => promiseMaker(1))
.then(() => promiseMaker(0));
よめ太郎「↑こうや」
娘「わあ」
娘「だいぶ非道鬼っぽさが減ったね!」
よめ太郎「せや。スッキリやろ?」
娘「うん」
娘「then().then().then()
って、繋げて書けるんだね」
よめ太郎「せや」
よめ太郎「then()
の返り値はpromiseオブジェクトやから、またthen()
メソッドを持ってんねん」
よめ太郎「せやから、どんどんthen()
を繋げて書けるんや」
よめ太郎「これがpromiseチェーンていうやつやな」
外部APIからデータを取得する例
よめ太郎「外部APIからのデータ取得なんかも非同期的な処理やから」
よめ太郎「連続してやろうとするとコールバック地獄になりがちなんやけど」
よめ太郎「Promise
を使えば大丈夫や」
娘「Promise
が無いとどうなるの?」
よめ太郎「ええとな」
よめ太郎「APIからデータを取得する場合は」
getApiData(apiUrl1, (response) => { /* データ取得後にやりたい処理 */ });
よめ太郎「こんな感じで、データ取得後にやりたい処理を」
よめ太郎「コールバック関数で渡さないといけないことが多いんや」
娘「そっか、非同期だもんね」
娘「タイミングを合わせるために」
「関数を渡しておくから、データ取り終わったら実行してね!」
娘「って感じでお願いするわけだもんね」
よめ太郎「せやせや」
よめ太郎「だから、連続して処理しようとすると・・・」
getApiData(apiUrl1, response1 => {
getApiData(apiUrl2, response2 => {
getApiData(apiUrl3, response3 => {
/* 最終的にやりたい処理をここに書く。 */
});
});
});
よめ太郎「↑こう、ネストしてしまうんや」
よめ太郎「でもPromiseを使うと・・・」
new Promise(resolve => {
getApiData(apiUrl1, response => resolve(response.userData));
})
.then(userData => nextFunc(userData));
よめ太郎「↑こんな感じで1回外に出て」
よめ太郎「then()
に渡す関数のところで次の処理を書けるから」
よめ太郎「ネスト地獄にはならへんのや」
よめ太郎「例えAPIを連続で叩いてもな」
娘「へえ〜」
娘「っていうか」
娘「resolve()
を実行する時に引数を渡すこともできるんだね」
よめ太郎「そうやで」
よめ太郎「むしろ引数を渡すことの方が多いで」
よめ太郎「外部APIからデータを取得して、そのデータをresolve()
に渡す」
よめ太郎「渡されたデータはthen()
に渡す関数で受け取る」
娘「じゃあ、then()
に渡す関数は」
娘「引数を受け取るように書かないとだね!」
よめ太郎「そうや」
.then(userData => nextFunc(userData));
よめ太郎「↑こんな感じやな」
娘「そっか」
娘「resolve()
実行時に渡した引数が」
娘「このuserData
っていう引数に入って来るんだね」
娘「無理やり擬人化すると・・・」
Promiseを擬人化してみた
プロミスくん「ああ〜、早く生まれたいな〜」
プロミスくん「ねぇ、そこの4歳娘ちゃん!」
プロミスくん「ワイを生成してや!」
プロミスくん「お役に立てるかもしれんで!」
娘「いいけど、何の役に立ってくれるの?」
プロミスくん「連続した非同期処理の扱いが得意や!」
プロミスくん「連続してAPIを叩く系の処理とか、上手く扱えるで!」
娘「え?助かる!」
娘「ちょうど連続してAPIを叩きたかったの!」
娘「プロミスくん、生成したい!」
プロミスくん「おお、もうすぐワイも誕生できるんやな!」
プロミスくん「ちなみに、どんな非同期処理を扱ってほしいの?」
娘「えっとね」
娘「↓この案件を扱ってほしいの!」
- APIからデータを取得してくる。
- 無事データを取得してこれたら、そのデータを元に2つ目のAPIを叩きたい。
プロミスくん「APIからデータを取ってこれたら」
プロミスくん「次はそのデータを元に別のAPIを叩きたい感じか!」
プロミスくん「よっしゃ!この案件、ワイが扱うで!」
娘「ありがと!」
プロミスくん「ほな、まず1つ目のAPIを叩く処理を関数として書いて」
プロミスくん「それをコールバック関数として渡しながらワイを生成してくれ!」
娘「分かった!」
娘「はい!コールバック関数を書いたよ!」
プロミスくん「あ、そのコールバック関数を実行するときに」
プロミスくん「resolve
ってものを渡すから」
プロミスくん「resolve
を受け取って実行するように書いといてくれ!」
娘「resolve
を受け取って、実行する・・・」
娘「実行する、ってことはresolve
は関数なんだね!」
プロミスくん「せや!」
娘「分かった!ちょっと書き換えるね!」
娘「でもresolve
はどのタイミングで実行するように書けばいいの?」
プロミスくん「APIデータ取得完了後に」
プロミスくん「resolve(apiData)
って感じで実行するように書いてくれ!」
娘「データ取得完了後にresolve(apiData)
ね!」
娘「そこは自分で書かなきゃいけないんだね!」
娘「はい!そういう関数に書き換えたよ!」
プロミスくん「ほな、その関数を渡して、ワイを生成してくれ!」
娘「出でよ!プロミスくん!」
プロミスくん「やっと誕生できたわ!」
プロミスくん「これで、無事データが取って来れた暁には」
プロミスくん「そのデータがresolve()
経由で次の処理に渡るで!」
娘「約束だからね!」
プロミスくん「おう!無事データが取れたらな!」
娘「了解!」
娘「あれ、でもまだ2つ目のAPIを叩く処理の内容を伝えてないよ!」
プロミスくん「せや!」
プロミスくん「ほな、続きの処理の内容を関数として書いて」
プロミスくん「ワイのthen()
メソッドに渡してや!」
娘「分かった!続きの処理はthen()
メソッドに渡すね!」
娘「1つ目の処理のその後に実行したい処理だから」
娘「then()
メソッド、っていう名前なんだね!」
プロミスくん「せや!」
娘「・・・はい!続きの処理の関数も書けたよ!」
娘「この関数は、最初のAPIデータを引数として受け取って」
娘「それを元に2つ目のAPIを叩く・・・」
娘「そんなテイで書いたよ!」
プロミスくん「それでオッケーや!」
プロミスくん「これをワイのthen()
メソッドに登録しておくで!」
プロミスくん「これで、最初のAPIからデータが取れた場合」
プロミスくん「そのデータを元に、2つ目のAPIが叩かれるはずや!」
娘「ありがとう!」
プロミスくん「おっ、最初のAPIからデータが無事に取得できたようやで!」
娘「あ、ちゃんと2つ目の処理も実行されてる!」
プロミスくん「よかったな!」
娘「うん!でも・・・」
娘「なんで関数を2個も書かないといけないの?」
娘「プロミスくんを生成する時に渡す関数と」
娘「then()に
渡す関数」
プロミスくん「いや、むしろ2つに分けるから」
プロミスくん「コールバック地獄にならへんねん」
娘「あ、そっか」
娘「1つの関数の中にネストして書くと地獄になっちゃうけど」
娘「プロミスくんがresolve()
経由で値をうまいこと渡してくれるから」
娘「then()
に渡す関数のところで続きの処理を書けるんだね」
プロミスくん「そういうことや!」
妄想終了
娘「・・・こんな感じだね」
よめ太郎「せやな」
よめ太郎「実際の場面としては・・・」
「やめ太郎のフォロワーさん一覧をAPIから取得や!」
「次は、そのフォロワーさん達が他に誰をフォローしているかAPIから取得や!」
「その情報を元に、また別のAPIを叩く!」
よめ太郎「・・・なんて処理をする時は、Promiseを使うと書きやすくなるかもな」
処理が失敗したときのことも書ける
よめ太郎「非同期処理は失敗することもあるから・・・」
プロミスくん「なんかエラーが出て失敗した場合にはどうすればいい?」
プロミスくん「エラーが起きた場合にやって欲しい処理があったら」
プロミスくん「その内容を関数にして、ワイのcatch()
メソッドに渡してや!」
よめ太郎「なんてこともしてくれるんやで」
娘「なんかthen()
に似てるね」
よめ太郎「そうそう」
よめ太郎「失敗した場合のthen()
みたいなもんや」
娘「そっか、APIからのデータ取得処理なんかは」
娘「必ず成功するわけじゃないもんね」
よめ太郎「せやせや」
よめ太郎「あの有名なaxios1のget()
メソッドなんかも」
よめ太郎「戻り値はpromiseオブジェクトやで」
よめ太郎「だから・・・」
axios.get('http://api.example.com/user/1')
.then(response => nextFunc(response))
.catch(error => console.log(error));
よめ太郎「↑こんな風に書けるんや」
娘「なるほどね〜」
よめ太郎「まだまだ奥が深いから」
よめ太郎「よかったら下の方に書いてある参考文献も読んでみてな!」
娘「うん!」
そういえばasync/awaitは?
娘「ママ、そういえば」
娘「async/awaitはどんな感じなの?」
よめ太郎「長くなり過ぎたから、次回説明するわ」
娘「その記事はいつ公開?」
よめ太郎「公開タイミングは分からん」
よめ太郎「非同期だけにな」
〜つづく〜
まとめ
- 入れ子になりそうなコールバック処理も、
Promise
を使ったら割と平坦に書けた。
参考文献
- Promiseを使う - JavaScript | MDN
- Promise - MDN - Mozilla
- Promise.prototype.then() - MDN - Mozilla
- Promise() コンストラクター - JavaScript | MDN
-
外部APIからデータを取得する為のライブラリ。 ↩