Promise/then・Observable・async/awaitの3者って、似たような文脈でよく出てくるけど、出くわすたびになんとなく書いてなんとかしてた感があったので、
同じ動作をするコードを書いてみて、書き方にどんな違いが出てくるか比較してみよう、という趣旨の記事です。
きっと理解が深まると思います。
3者の関係を説明しておくと、
-
Promiseの扱い方の中に、thenを使う方法とasync/awaitを使う方法がある -
Observableの扱いは、Promiseの「thenを使う方法」と似ている
という感じです。
thenとasync/awaitはどちらもPromiseを扱う方法で、PromiseとObservableは似ていますが違うものです。
(2020-04-18)改定のお知らせ
筆者の理解が深まったため、記事を全面改訂しました。
- 「
asyncの場合」に、asyncを使えるのに使っていなかったところでasyncを使用した。 - エラーハンドリングのコードを、3パターンでちゃんと同じ動作をするようにした。
- それに伴い解説文もいろいろ変更
環境
node v8.9.4
rxjs v6.5.5
バージョン的な留意点としては、
nodeでは未だにES Moduleがデフォルトで機能しないということと
rxjsはoperatorを全部pipe()の中に入れる書き方になった後のバージョンだっていう
そのあたりですかね。
通常処理
どんなことやらせるの?
AはBを呼んでその結果を使う処理。
BはCを呼んでその結果を使う処理です。
AはBの結果を待ち、BはCの結果を待つわけですが、
Cは、時間がかかったり、エラーになる可能性がある処理です。
そんな感じ。
処理の発生はA→B→Cの順ですが、完了はC→B→Aの順になっておりまして、
コード内のコメントは下から読んだほうがわかりやすいです。
Promise/thenの場合
ではコードを見ていきましょう。
// functionBの結果を待って、?????を付けて画面に出力
const functionA = () => {
functionB().then((x) => console.log(x + "?????"));
};
// functionCの結果を待って、中身の末尾に!!!!!を付けた、新しいPromiseをreturn
const functionB = () => {
return functionC().then((x) => x + "!!!!!");
};
// 1秒待機して、OH YEAHをresolveする(Promiseに包んでreturnする)
const functionC = () => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve("OH YEAH"), 1000);
});
};
functionA();
コードの最後にfunctionA()を呼び出して全体を実行しています。
ではこのコードを実行してみましょう。
$ node a_promise.js
OH YEAH!!!!!?????
1秒の待機の後、こちらが出力されました。
functionBとfunctionAによる修飾(!!!!!と?????を付け足す)がちゃんと機能してますね。
見ておきたいポイントはfunctionBです。
一度thenで値を取り出した後、thenに渡した関数の中でx+"!!!!!"をreturnしています。
(アロー関数になっていてreturnという文字はないですが、=>の右側がreturnされる中身です。)
そしてそのreturnされたモノを中身として持つPromiseが、thenの返り値になります。
returnされた中身を改めてPromiseで包んで、全体(functionB)の返り値にしてるというイメージです。
では次。
Observableの場合
コードを見てみます。
注意:nodeで実行するための記述がいくつかあります。本筋からそれるのでここでは解説を省略します。
- ファイル名の拡張子が
.mjsなこと - import文
- 実行コマンドのオプション
--experimental-modules - 出力の一行目
npm install rxjsは済ませてあります。
import rxjs from "rxjs";
const { Observable } = rxjs;
import operators from "rxjs/operators";
const { map } = operators;
// functionBの結果を待って、?????を付けて画面に出力
const functionA = () => {
functionB().subscribe((x) => console.log(x + "?????"));
};
// functionCの結果を待って、Observableの中身に!!!!!をつける改造を施してreturn
const functionB = () => {
return functionC().pipe(map((x) => x + "!!!!!"));
};
// 1秒待機して、observer.nextを呼ぶ(OH YEAHをObservableに包んでreturnする)
const functionC = () => {
return Observable.create((observer) => {
setTimeout(() => observer.next("OH YEAH"), 1000);
});
};
functionA();
実行してみます。
$ node --experimental-modules a_observable.mjs
(node:17813) ExperimentalWarning: The ESM module loader is experimental.
OH YEAH!!!!!?????
1秒の待機の後、こちらが出力されました。(関係ない一行目を無視すると)さっきと全く同じですね。
コードを見ると、
functionBの中身が、.then()じゃなくて.pipe(map(())になってますね。
thenは、Promiseの中身を受け取る時に使うメソッドですが、pipeはObservableの中身を受け取る時に使うメソッドではありません。
Observableの中身を受け取るときはsubscribeを使いますからね。
Promise/thenの場合と比べると、thenが一度Promiseの中身を受け取るのに対して、pipeはObservableの外から中身にだけ改造を施すようなイメージです。その改造に使うツールをoperatorと言って、pipeの引数に渡して使います。
map(x=>x+"!!!!!")は、「来たxをx+"!!!!!"に改造する」というoperatorです。
で、pipeの返り値はまたObservableになります。
ちなみにpipeの中には複数のoperatorを入れることができ、書いた順に適用されます。
async/awaitの場合
// functionBの結果を待って、?????を付けて画面に出力
const functionA = async () => {
console.log((await functionB()) + "?????");
};
// functionCの結果を待って、中身の末尾に!!!!!を付けた、新しいPromiseをreturn
const functionB = async () => {
return (await functionC()) + "!!!!!";
};
// 1秒待機して、OH YEAHをresolveする(Promiseに包んでreturnする)
const functionC = async () => {
return await new Promise((resolve, reject) => {
setTimeout(() => resolve("OH YEAH"), 1000);
});
};
functionA();
実行します。
$ node a_async.js
OH YEAH!!!!!?????
1秒の待機の後、こちらが出力されました。こちらも全く同じです。
コードの見た目が結構変わりましたね。
async関数は、returnされた値をPromiseに包んで返します。
Promiseで包むということは、その中身は最初から判明してなくてもいいわけなので、その中で「何かを待ってから値を返す」という処理を書くことができるようになります。それがawaitです。
await functionC()のように書くと、これは**functionC()が返すPromiseの中身(resolveした値)を表します**。
中身が判明していなければ、判明するまで待機します。
今回のfunctionBの動きを確認すると、
functionCの中身が判明するのを待ってから、中身に"!!!!!"を付け足しています。
それを用いて、先にfunctionAに渡していたPromiseの中身を判明させる、という動きをしているわけです。
functionCの動きも一見わかりにくいので説明すると、
new Promiseで一秒後に解決するPromiseを作り、awaitでその中身を(解決を待ってから)取り出し、returnで返すのですが、asyncなのでまたPromiseに包まれます。
結局、functionCの動きはこれまでの例と変わっていません。
エラーハンドリング
エラーハンドリングの書き方の違いも確認しておきましょう。
どんなことやらせるの?
先程のコードの機能は残したまま、functionCがエラーを送出するようにして、それをfunctionBでハンドリングします。
functionBはエラーを画面に出力したあと、替わりの文字列(エラーではなく!)をfunctionAに返します。
functionAにもエラーハンドリング機能がついていますが、こちらは使用されません。
functionCが普通の文字列を返す場合、コード全体が先程と全く同じ動きをします。
Promise/thenの場合のエラーハンドリング
// functionB()でエラーハンドリングされていない場合は、ここでエラーハンドリングされる
const functionA = () => {
functionB()
.then((x) => console.log(x + "?????"))
.catch((e) => console.log(e));
};
// ここでエラーハンドリング。エラーを画面出力し、
// 替わりに文字列"There was an error."に"!!!!!"を付けて送り返す。
const functionB = () => {
return functionC()
.catch((e) => {
console.log(e);
return "There was an error.";
})
.then((x) => x + "!!!!!");
};
// 1秒待機してrejectする(エラーの発生)
const functionC = () => {
return new Promise((resolve, reject) => {
// setTimeout(() => resolve("OH YEAH"), 1000);
setTimeout(() => reject("THIS IS THE ERROR"), 1000);
});
};
functionA();
$ node e_promise.js
THIS IS THE ERROR
There was an error.!!!!!?????
1秒の待機のあと、不思議な出力が出ました。
functionCが発生させたエラーはfunctionBの中でキャッチされ、コンソールに出力されています。
その後functionBは、エラーを"There was an error."という正常な文字列で置き換え、通常通り"!!!!!"を付けて返すので、
functionAはそれにさらに"?????"を付けて出力しています。
functionAのエラーハンドリング機能は使われません。
Observableの場合のエラーハンドリング
import rxjs from "rxjs";
const { Observable } = rxjs;
import operators from "rxjs/operators";
const { map, catchError } = operators;
// functionB()でエラーハンドリングされていない場合は、ここでエラーハンドリングされる
// subscribe()の二つ目の引数は、エラーをハンドリングする関数。
const functionA = () => {
functionB().subscribe(
(x) => console.log(x + "?????"),
(e) => console.log(e)
);
};
// ここでエラーハンドリング。エラーを画面出力し、
// 替わりに文字列"There was an error."に"!!!!!"を付けて送り返す。
const functionB = () => {
return functionC().pipe(
catchError((e) => {
console.log(e);
return ["There was an error."];
}),
map((x) => x + "!!!!!")
);
};
// 1秒待機してobserver.errorを呼ぶ(エラーの発生)
const functionC = () => {
return Observable.create((observer) => {
// setTimeout(() => observer.next("OH YEAH"), 1000);
setTimeout(() => observer.error("THIS IS THE ERROR"), 1000);
});
};
functionA();
$ node --experimental-modules e_observable.mjs
(node:46592) ExperimentalWarning: The ESM module loader is experimental.
THIS IS THE ERROR
There was an error.!!!!!?????
一行目を除けば、Promise/thenの場合と同じ出力です。
functionB内のfunctionC().pipe(...)の中で,
mapより先にcatchErrorという処理をつけることでエラーハンドリングしています。
functionCの返り値がエラーじゃない場合は、このcatchErrorは無視されます。
catchErrorの返り値の文字列が[]に入っているのは、こうしないと文字が一文字ずつ送出されてしまうためです。
async/awaitの場合のエラーハンドリング
// functionB()でエラーハンドリングされていない場合は、ここでエラーハンドリングされる
const functionA = async () => {
console.log((await functionB().catch((e) => console.log(e))) + "?????");
};
// ここでエラーハンドリング。エラーを画面出力し、
// 替わりに文字列"There was an error."に"!!!!!"を付けて送り返す。
const functionB = async () => {
return (
(await functionC().catch((e) => {
console.log(e);
return "There was an error.";
})) + "!!!!!"
);
};
// 1秒待機してrejectする(エラーの発生)
const functionC = async () => {
return await new Promise((resolve, reject) => {
// setTimeout(() => resolve("OH YEAH"), 1000);
setTimeout(() => reject("THIS IS THE ERROR"), 1000);
});
};
functionA();
$ node e_async.js
THIS IS THE ERROR
There was an error.!!!!!?????
Promise/thenの場合と同様に、functionC().catch(...)という構文でエラーハンドリングをします。
functionC()の返り値が正常な値なら.catchは無視され、エラーなら.catchにより正常な中身で置き換えられるので、その中身をawaitで取り出すことができます。
その後"!!!!!"を付け、functionAに返します。
結論、3者で何が違った?
では最後に、改めて、3者で何が違ったか確認して、この記事を終わっておきましょう。
次の点に注目して確認すると良いと思います。
-
thenの場合とObservableの場合で書き方が似ていること -
async/awaitを使って書き方を簡略化できていること
PromiseとObservableの作り方
// Promise(普通に)
const functionC = () => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve("OH YEAH"), 1000);
});
};
// Observable
const functionC = () => {
return Observable.create((observer) => {
setTimeout(() => observer.next("OH YEAH"), 1000);
});
};
// Promise(async/awaitで)
const functionC = async () => {
return await new Promise((resolve, reject) => {
setTimeout(() => resolve("OH YEAH"), 1000);
});
};
Promiseを作る場合は、コンストラクタに「(resolve,reject)という2つの関数のセットを受け取る関数」を渡します。
Observableを作る場合は、createメソッドに「observerを受け取る関数」を渡します。observerはnext``error``completeという3つの関数を持つオブジェクトです。
参考→RxJS基礎中の基礎
要は、いくつかの関数を受け取る関数を渡せば良いわけですね。
Promiseで言うところのresolveと、Observableで言うところのnextまたはcompleteが似たような働きをします。
エラーの場合はrejectまたはerrorを使います。
ただ、PromiseやObservableって、自分で作るケースよりも、何かしらのライブラリが返してきてそれを使うケースの方が多いので、作り方の違いはあんまり重要じゃないかもしれません。
あと、このケースだけ、async/awaitを使った結果、書き方が冗長になってます。結果を使う方ではちゃんと簡略化されてますのでお許しください。
結果の受け取り方
// Promise/then
functionB().then((x) => console.log(x + "?????"));
//Observable
functionB().subscribe((x) => console.log(x + "?????"));
// async/await
console.log((await functionB()) + "?????");
Promiseはthenメソッドに、Observableはsubscribeメソッドに、中身を受け取る関数を渡すことができます。
さらに、Promiseの場合はawait functionB()という書き方を中身そのものとして扱うことができます。
中身をいじって次にわたす方法
// Promise/then
return functionC().then((x) => x + "!!!!!");
// Observable
return functionC().pipe(map((x) => x + "!!!!!"));
// async/await
return (await functionC()) + "!!!!!";
Observableで中身を改造するには、pipeにoperatorを渡せばよいです。この場合subscribeは使いません。
エラーハンドリング
// then/catch
return functionC()
.catch((e) => {
console.log(e);
return "There was an error.";
})
.then((x) => x + "!!!!!");
// Observable
return functionC().pipe(
catchError((e) => {
console.log(e);
return ["There was an error."];
}),
map((x) => x + "!!!!!")
);
// async/await
return (
(await functionC().catch((e) => {
console.log(e);
return "There was an error.";
})) + "!!!!!"
);
Promiseの場合は、.catchを付けてエラーハンドリングします。.catchは、エラーじゃない場合は無視され、コードの動きに影響しません。
Observableの場合は、pipeの中にcatchErrorというオペレーターを加えます。これもエラーじゃない場合は無視されます。
おわり
こんな所ですかね。
ちなみにこの記事では触れてませんが、Promiseの場合のエラーハンドリング関数は、Observablelの.subscribe()の時と同じように.then()の第二引数に渡してもいいです。
宣伝
この記事を読んでいる人のほとんどは、日々作業を抱えている人だと思います。
みんなで、サボらずに、メリハリをつけて作業に取り組むためのwebサービスをReact+NextJSで作成しております。
ぜひ遊びに作業しに来てください!