8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Make ITAdvent Calendar 2018

Day 3

Promiseについてなんかいい感じに書く

Last updated at Posted at 2018-12-04

12/3に間に合いませんでした。 ゆるして ごめんなさい。


前書き という名の保身

先に保身だけ・・・

  • Q:なんで今さら?

  • A:サークル内での活動中で需要がありそうだったから😇

  • Q:サンプルコードが〜

  • A:適当だから突っ込まないで温かい目で見て😇

  • Q:解りづらい

  • A:諦めて😇 もしくは promiseの本 読もうな

  • Q:最初のくだりいらない

  • A:読んで 本題 ←はい


JSの闇

よし

  1. APIから値取ってきて
  2. その値をごにょごにょして別のAPIから値取って
  3. さらにそれを元に別のAPIから値取って表示しよう!
  4. エラーが出たら中断してエラー表示するぞ!
	//(細かい処理とかは省略...)
	getFromAPI(request, (err, data) => {
		if (err) {
			console.error(err);
			return;
		}
		getFromOtherAPI(request, data, (err, secondData) => {
			if (err) {
				console.error(err);
				return;
			}
			getFromSomeOtherAPI(request, secondData, (err, lastData) => {
				if (err) {
					console.error(err);
					return;
				}
				viewRendering(lastData);
			});
		});
	});

😇 <見づれぇ

2個めのAPIの結果を別の場所でも使うことになったよ!

	let apiData;
	getFromAPI(request, (err, data) => {
		if (err) {
			console.error(err);
			return;
		}
		getFromOtherAPI(request, data, (err, secondData) => {
			if (err) {
				console.error(err);
				return;
			}
			apiData = secondData;
			getFromSomeOtherAPI(request, secondData, (err, lastData) => {
				if (err) {
					console.error(err);
					return;
				}
				viewRendering(lastData);
			});
		});
	});

	// ...

	otherFunction(apiData);
	
	// ...

😇 <これもう解かんねえな

というわけで、JSには通称 コールバック地獄 と呼ばれる大きな闇があります。

非同期的な処理を同期させて書こうとすると値が返ってから処理をするために
  コールバック in コールバック in コールバック in・・・
とどんどんネストが深くなってしまう問題です。

そして向かった先

当然、糞みたいなコールバック地獄を避けるために先人たちは色々なアイデアを考えてきました。

  • jquery.deferred
  • async.js
  • generator
  • Concurrent.Thread

etc...
まぁ歴史が深いので気になる人は@KDKTNさんの
コールバック……駆逐してやる…この世から…一匹…残らず!! - Qiita
とか読んでみると面白いかもしれません。

さてさて、そんなこんな色々合った後にいよいよ先人たちは素晴らしいものを生み出しました。

Promise

さてさて、お待たせしました。本題です。

Promise...まずはみんな大好きMDNの説明読んでみましょう

Promise オブジェクトは非同期処理の最終的な完了処理(もしくは失敗)およびその結果の値を表現します。
Promise - JavaScript|MDN

うん、よくわからないね

というわけでとりあえず上のサンプルコードをPromise使った形に直してみます。

getFromAPI(request)
	.then(getFromOtherAPI)
	.then(getFromSomeOtherAPI)
	.then(viewRendering)
	.catch((err) => {
		console.error(err);
	});

はい。こんだけ。

え?あのクソ長いコールバック地獄は?thenって何?戻り値は?引数は?

はい。一個一個説明してきましょう!
まずPromiseとは。

結論から言うと、

Promiseは中に書いた処理を非同期的に実行し、完了または失敗した際に結果を呼び出し元に返すオブジェクトです。

うーん、MDNの説明と変わりませんね。

とりあえず実際にPromiseオブジェクト(以下promise)を書いてみたいと思います。

const testFunction = (arg) => {
	return new Promise((resolve, reject) => {
		if (arg === true) {
			resolve("success");
		} else {
			reject("failed");
		}
	});
}

はい、呼び出されると引数がtrueなら"success"を、falseなら"failed"を返すpromiseです。

一行ずつ追ってみましょう。

const testFunction = (arg) => {

普通にアロー関数で関数定義。 引数は普通にここに書きます。

return new Promise((resolve, reject) => {

さて、いきなりですが重要なとこです。
関数自体の戻り値に new Promiseして生成したpromiseを返します。
promise自体の引数にはresolveとrejectという2つの引数を入れたコールバック関数が入ります。
promiseでは、このコールバック関数の中に実際に行われる処理を記述します。

そして、処理が完了したときにコールバック関数に与えた

resolve("success");

を呼び出すことで、大本の関数の戻り値に処理が終了し完了した、という状態と、resolve()に与えた引数の値が返されます。
今回の場合は"success"という値が戻ります。

また、処理が失敗したときには

reject("failed");

を呼ぶことで、resolveと同じように処理そのものは終了したが、失敗したという状態とreject()に与えた引数の値が返されます。

実はpromise自体の説明としてはこんなもので終わりで、関数の中身をnew Promiseでラップしてあげてreturnをresolveに変えるだけと、結構簡単です。

では、次にpromise関数を呼び出すときの説明をしたいと思います。

実際に今作った関数を例にしてみます。
まずは普通に呼んでみます。

const result = testFunction(true);

console.log(result);

さて、console.log(result)の結果では何が出力されるでしょうか?
もし手元に環境があるならブラウザでもNode.jsのREPLでも試してみてください。
おそらく普通の関数だったら"success"が帰ることでしょう。
ですが、promiseの場合は違います。

実際に手元の環境で実行してみると、このようにでました。

Promise {
  'success',
  domain:
   Domain {
     domain: null,
     _events:
      { removeListener: [Function: updateExceptionCapture],
        newListener: [Function: updateExceptionCapture],
        error: [Function: debugDomainError] },
     _eventsCount: 3,
     _maxListeners: undefined,
     members: [] } }

はい、なんか"success"の文字は見えるけど全然違いますね。
これはさっきも書いたとおり、関数の戻り値自体はあくまでpromiseであり、promiseの中の戻り値では無いからです。

ではどうしたらいいのでしょうか?

promiseから戻り値を得るためには、then()というメソッドを使います。
promiseからthenメソッドを呼ぶことで、thenの中のコールバック関数にpromise内での戻り値が渡ります。

コードにすると

const promiseFunction = testFunction(true);
const result = promiseFunction.then((result) => {return result});
console.log(result); // "success"

となります。

え? 

  • さっきより手間増えてる上に余計解りづらい?
  • あとrejectの時はどうなるの?

OK、まとめてコードにして話しましょう。

testFunction(true)
	.then((result) => {
		console.log(result); // "success"
	})
	.catch((err) => {
		console.error(err); // "failed"
	});

thenメソッドは、メソッドチェーンと呼ばれる記法で書くことができます。
それを利用して書くと、このように、testFunctionの戻り値のpromiseを変数に吐き出さなくても、
testFunction自体にそのまま戻り値であるpromiseのthenメソッドを使うことができるのです。
これで大分見やすくなりましたね。

では続いて新しく登場したcatchについても説明します。
おそらく察しのいい人は気づいているかもしれませんが、こっちはreject際に呼び出されるメソッドです。
用途的にはエラーハンドリングのとき等に使用します。

つまり、resolve, reject, then, catchの関係をまとめると

  • 処理が成功した!
    • => thenが呼び出される
  • 処理が失敗した!
    • => catchが呼び出される

といった感じです。 シンプル!

もうこれで大体promiseを呼び出して使うことは出来るようになりま・・・

え? これじゃ結局コールバック地獄じゃないかって?
確かにこのままだと最初の処理を書き直しても

getFromAPI(request)
	.then((data) => {
		getFromOtherAPI(request, data)
			.then((secondData) => {
				getFromSomeOtherAPI(request secondData)
					.then((lastData) => {
						viewRendering(lastData);
					})
					.catch((err) => {
						console.error(err);
					});
			})
			.catch((err) => {
				console.error(err);
			});
	})
	.catch((err) => {
		console.error(err);
	});

oh...最初よりも酷くなってしまいました。

え?じゃあなんでpromiseの説明なんかしたのかって?
もちろんpromiseの便利な使い方はこんなものでは無いからです!

promiseチェーン!

さて、そのためにはpromiseのresolve,rejectとthenメソッドについて、もう二つほどお話をしなければいけません。

さて、そのためには、resolve, rejectが一体何をしているのかを説明する必要があります。
さっき、resolveは

大本の関数の戻り値に処理が終了し完了した、という状態と、resolve()に与えた引数の値が返されます。
今回の場合は"success"という値が戻ります。

と書きました。
ですが、実は一箇所だけ説明を簡単にするために大雑把に書きました。
正確に上の説明を書くとこうなります。

大本の関数の戻り値に処理が終了し完了した、という状態と、resolve()に与えた引数の値を持ったpromiseが返されます。

はい。正確にはresolveを呼んだ際に返却されるのはまたpromiseとなるのです。
同様に、rejectの際も失敗したという状態と値を持ったpromiseとなります。

で?結局なにが変わったのかって?

これを知った上で、改めてthenメソッドについて考えて見てください。
resolve/rejectされて返却されるものがpromiseということは、thenメソッドはこれまでpromiseを受け取っていたことになります。
ですが、promiseに含まれている値だけをコールバック関数で受け取っていました。

はい、つまりthenはメソッドチェーンで呼び出された時に、親のpromiseが完了した際の値だけを受け取るということです(正確には違いますが、今はこれで大丈夫です。)

次に、thenそのものについてです。

手っ取り早く、結論から先に言ってしまいましょう。

thenメソッドは中のコールバック関数内でreturnされた場合、returnした値を持った完了状態のpromiseを返します。

つまり、thenメソッドで戻り値を設定した場合、次に呼ばれるメソッドには完了状態を持ったpromiseが入るのです。

これまた、察しのいい人は気づいたのではないでしょうか?

thenはメソッドチェーンで呼び出された時に、親のpromiseが完了した際の値だけを受け取るということです。

そうです。thenはいくらでも繋げることができます。

testFunction(true)
	.then((result) => { // "success"
		result = result + " and go through in then method!";
		return result
	})
	.then((result) => {
		console.log(result); // "success and go through in then method!"
	})

これを
Promiseチェーン と呼んでいます。

これを使えば、さっきのサンプルも・・・

getFromAPI(request)
	.then((data) => {
		return getFromOtherAPI(request, data)
	})
	.then((secondData) => {
		return getFromSomeOtherAPI(request, secondData);
	})
	.then((lastData) => {
		viewRendering(lastData);
	})
	.catch((err) => {
		console.error(err);
	});

超スッキリ!!!!!

これこそがコールバック地獄の解決策としてpromiseを使う一番の理由です。

さて、追加で説明をしておくと、一つのPromiseチェーンのどこかでrejectが呼ばれた場合、
そのメソッドより下にあってかつ一番上のcatchが自動で呼び出されます。
そのとき、途中にあるメソッドは無視してcatchされます。

上の例を少し弄って説明すると

getFromAPI(request)
	.then((data) => {
		return getFromOtherAPI(request, data) // ここでrejectが呼ばれる
	})
	.then((secondData) => { // ここは通らない
		return getFromSomeOtherAPI(request, secondData);
	})
	.then((lastData) => { // ここも通らない
		viewRendering(lastData);
	})
	.catch((err) => { // ここに入る
		console.error("0:" + err); // "0: Error: cannot get From API
	})
	.catch((err) => { // ここにも入らない
		console.error("1:" + err);
	});

といった感じです。

大体こんなノリ と あとがき


とまぁ、だいたいこんなノリで進めてきましたが、とりあえず予定日過ぎてる(ここ執筆時点で12/4 12:00)上に記事の量も400行を超えたので、
ココらへんで一旦締めようかな と思います。

ひとまず今回の記事で説明したことが理解できれば、基本的なpromiseは使えるようになったのでは無いかな と思います。(自信過剰)

ですが、ここまで読んである程度promiseを知っている人は足りないところに気づくでしょう。

  • .allと.raceどこ?
  • async/awaitは?
  • いやもっと実用例的なの書けよ

...はい、遅れた上に時間不足と量が多すぎて書ききれないという大ポカやらかしました😇

もちろん頭の中にはありますので、次回の僕のアドベントカレンダー(12/10)に上げたいと思います。

それでは、12/3(過ぎたけど)のMake IT Advent Calendar 2018、お楽しみ頂けたでしょうか?
お楽しみ頂けた、少しでも勉強になったなら幸いです。

ここまでご愛読頂き、ありがとうございました!

P.S. わからないところ、マサカリ等有りましたらコメント頂けたら嬉しいです!

8
5
1

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
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?