Promise・Observable・async/await のそれぞれで同じ事をやってみて書き方の違いを理解しよう
Promise・Observable・async/await のそれぞれで同じ事をやってみて書き方の違いを理解しよう:
同じ動作をするコードを書いてみて、書き方にどんな違いが出てくるか比較してみよう、という趣旨の記事です。
きっと理解が深まると思います。
node v8.9.4
rxjs v6.3.3
バージョン的な留意点としては、
nodeでは未だにES Moduleがデフォルトで機能しないということと
rxjsはoperatorを全部
そのあたりですかね。
AはBを呼んでその結果を使う処理。
BはCを呼んでその結果を使う処理です。
AはBの結果を待ち、BはCの結果を待つわけですが、
Cは、時間がかかったり、エラーになる可能性がある処理です。
そんな感じ。
処理の発生はA→B→Cの順ですが、完了はC→B→Aの順になっておりまして、
コード内のコメントは下から読んだほうがわかりやすいです。
ではコードを見ていきましょう。
コードの最後に
ではこのコードを実行してみましょう。
1秒の待機の後、こちらが出力されました。
functionBとfunctionAによる修飾(!!!!!と?????を付け足す)がちゃんと機能してますね。
見ておきたいポイントは
一度
(アロー関数になっていて
そしてその
では次。
ファイル名の拡張子が
実行してみます。(--experimental-modulesについてはここでは説明しないので気になる方はググってください)
1秒の待機の後、こちらが出力されました。さっきと全く同じですね。(いや、同じじゃない行も出てますが、nodeがmodulesに対応してない関係で出てるだけで、まあ、無視していいです。)
コードを見ると、
Observableの中身を受け取るときは
で、
結局、「一度中身を受け取る」か「受け取らずに中身だけ改造する」かのイメージの違いはあれど、コードと動きはあんまり変わりませんね。中身の変わったPromiseかObservableが返ってるだけですからね。
実行します。
1秒の待機の後、こちらが出力されました。こちらも全く同じです。
コードの見た目が結構変わりましたね。
違うのはやはり
さっきまでfunctionAとfunctionBはアロー関数を使って定義していましたが、
今回は
Promiseで包むということは、その中身は最初から判明してなくてもいいわけなので、その中で「何かを待ってから値を返す」という処理を書くことができるようになります。それが
中身が判明していなければ、判明するまで待機します。
今回の
エラーハンドリングの書き方の違いも確認しておきましょう。
といっても、さっきまでのコードにすでにエラーハンドリング用のコードは入っていますので、
違うのは、
不思議な出力が出ました。
そこでエラーハンドリングは完了しており、
そして、そのundefinedを文字列として解釈して、それに"?????"をつけるなんていう、javascriptらしい珍妙な動きをしているわけです。
本当は
このあたり読者の皆さんどう思いますか。
次いきます。
今度は普通にTHIS IS THE ERRORが一回出ただけで終わりましたね。Warningの行は例によって無視してください。
ではどうやってエラーハンドリングするかと言うと
そこまでの処理でエラーが発生していれば、ここまで伝播しています。
なので、この
コードの最後に
また、この記事では紹介していませんが、
では最後に、改めて、3者で何が違ったか確認して、この記事を終わっておきましょう。
Promiseを作る場合は、コンストラクタに「(resolve,reject)という2つの関数のセットを受け取る関数」を渡します。
Observableを作る場合は、createメソッドに「observerを受け取る関数」を渡します。observerは
参考→RxJS基礎中の基礎
要は、いくつかの関数を受け取る関数を渡せば良いわけですね。
Promiseで言うところの
エラーの場合は
ただまあ、PromiseやObservableって、自分で作るケースよりも、何かしらのライブラリが返してきてそれを使うケースの方が多いと思うんですよね。
ですから、作り方の違いはあんまり重要じゃないかもしれません。むしろ以下の方が重要。
こんな所ですかね。
ちなみにこの記事では触れてませんが、Promiseの場合のエラーハンドリング関数は、Observablelの
Promie・Observable・async/awaitの3者って、似たような文脈でよく出てくるけど、出くわすたびになんとなく書いてなんとかしてた感があったので、同じ動作をするコードを書いてみて、書き方にどんな違いが出てくるか比較してみよう、という趣旨の記事です。
きっと理解が深まると思います。
環境
node v8.9.4rxjs v6.3.3
バージョン的な留意点としては、
nodeでは未だにES Moduleがデフォルトで機能しないということと
rxjsはoperatorを全部
pipe()の中に入れる書き方になった後のバージョンだっていうそのあたりですかね。
どんなことやらせるの?
AはBを呼んでその結果を使う処理。BはCを呼んでその結果を使う処理です。
AはBの結果を待ち、BはCの結果を待つわけですが、
Cは、時間がかかったり、エラーになる可能性がある処理です。
そんな感じ。
処理の発生はA→B→Cの順ですが、完了はC→B→Aの順になっておりまして、
コード内のコメントは下から読んだほうがわかりやすいです。
Promiseの場合
ではコードを見ていきましょう。promise.js
const functionA = () => {
functionB().then(x=>console.log(x+"?????")).catch(e=>console.log(e));
}// functionBの結果を待って、?????を付けて画面に出力
const functionB = () => {
return functionC().then(x=>x+"!!!!!").catch(e=>console.log(e));
}// functionCの結果を待って、中身の末尾に!!!!!を付けた、新しいPromiseをreturn
const functionC = () => {
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("OH YEAH");
},1000);
});
}// 1秒待機して、OH YEAHをresolveする(Promiseに包んでreturnする)
functionA();
functionA()を呼び出して全体を実行してます。ではこのコードを実行してみましょう。
$ node promise.js
OH YEAH!!!!!?????
functionBとfunctionAによる修飾(!!!!!と?????を付け足す)がちゃんと機能してますね。
見ておきたいポイントは
functionB()です。一度
.then()で値を取り出した後、then()に渡した関数の中でx+"!!!!!"をreturnしています。(アロー関数になっていて
returnという文字はないですが、=>の右側がreturnされる中身です。)そしてその
returnされたモノを中身として持つPromiseが、then()の返り値になります。returnされた中身を改めてPromiseで包んで、全体(functionB)の返り値にしてるというイメージです。では次。
Observableの場合
ファイル名の拡張子が.mjsなのと、import文が何か変なのは、nodeで実行するためのおまじないだと思ってください。本筋からそれるのでここでは解説しません。npm install rxjsは済ませてあります。observable.mjs
import rxjs from 'rxjs';
const { Observable } = rxjs;
import operators from 'rxjs/operators';
const { map } = operators;
const functionA = () => {
functionB().subscribe(x=>console.log(x+"?????"),e=>console.log(e));
}// functionBの結果を待って、?????を付けて画面に出力
const functionB = () => {
return functionC().pipe(map(x=>x+"!!!!!"));
}// functionCの結果を待って、Observableの中身に!!!!!をつける改造を施してreturn
const functionC = () => {
return Observable.create(observer=>{
setTimeout(()=>{
observer.next("OH YEAH");
},1000);
});
}// 1秒待機して、observer.nextを呼ぶ(OH YEAHをObservableに包んでreturnする)
functionA();
$ node --experimental-modules observable.mjs
(node:17813) ExperimentalWarning: The ESM module loader is experimental. OH YEAH!!!!!?????
コードを見ると、
functionB()の中身が、.then()じゃなくて.pipe(map(())になってますね。.then()は、Promiseの中身を受け取る時に使うメソッドですが、.pipe()はObservableの中身を受け取る時に使うメソッドではありません。Observableの中身を受け取るときは
.subscribe()を使いますからね。.then()が一度Promiseの中身を受け取るのに対して、.pipe()はObservableの外から中身にだけ改造を施すようなイメージです。その改造に使うツールをoperatorと言って、.pipe()の引数に渡して使います。map(x=>x+"!!!!!")というのは、「来たxをx+"!!!!!"に改造する」ということですね。で、
.pipe()の返り値はまたObservableになります。結局、「一度中身を受け取る」か「受け取らずに中身だけ改造する」かのイメージの違いはあれど、コードと動きはあんまり変わりませんね。中身の変わったPromiseかObservableが返ってるだけですからね。
async/awaitの場合
async.js
async function functionA(){
const x = await functionB();
console.log(x+"?????");
}// functionBの結果を待って、?????を付けて画面に出力
// async functionは必ずPromiseを返すので、実はfunctionA()はPromise<void>を返している
async function functionB(){
const x = await functionC();
return x + "!!!!!";
}// functionCの結果を待って、中身の末尾に!!!!!を付けた、新しいPromiseをreturn
const functionC = () => {
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("OH YEAH");
},1000);
});
}// 1秒待機して、OH YEAHをresolveする(Promiseに包んでreturnする)
functionA().catch(e=>console.log(e));
$ node async.js
OH YEAH!!!!!?????
コードの見た目が結構変わりましたね。
functionC()については、最初のPromiseの例とまったく同じです。違うのはやはり
functionB()です。functionA()も結構違います。さっきまでfunctionAとfunctionBはアロー関数を使って定義していましたが、
今回は
async functionという宣言を使いたいので、アロー関数は使っていません。async functionという宣言で定義された関数は、returnされた値をPromiseに包んで返します。Promiseで包むということは、その中身は最初から判明してなくてもいいわけなので、その中で「何かを待ってから値を返す」という処理を書くことができるようになります。それが
awaitです。x = await functionC()のように書くと、xには、functionC()の返り値であるPromiseではなく、そのPromiseの中身(resolveした値)が入ります。中身が判明していなければ、判明するまで待機します。
今回の
functionB()の動きを確認すると、functionC()の中身が判明するのを待ってからxに代入し、そのxに"!!!!!"を付け足して、先にfunctionA()に渡していたPromiseの中身を判明させる、という動きをしているわけです。
エラーハンドリング編
エラーハンドリングの書き方の違いも確認しておきましょう。といっても、さっきまでのコードにすでにエラーハンドリング用のコードは入っていますので、
違うのは、
functionC()の中でエラーを発生させている所だけです。
Promiseの場合のエラーハンドリング
e_promise.js
const functionA = () => {
functionB().then(x=>console.log(x+"?????")).catch(e=>console.log(e));
}// functionB()の中身(undefined)に、文字列"?????"を強引に足した結果が画面に出る"
// functionB()でエラーハンドリングされていない場合は、ここでエラーハンドリングされる(THIS IS THE ERRORが画面に出る)
const functionB = () => {
return functionC().then(x=>x+"!!!!!").catch(e=>console.log(e));
}// ここでエラーハンドリングが完了し、functionB()はundefinedが入ったPromiseを返す
// ここでcatchしなかった場合は、functionA()の方までエラーが伝播し、キャッチされる。
const functionC = () => {
return new Promise((resolve,reject)=>{
setTimeout(()=>{
reject("THIS IS THE ERROR");
},1000);
});
}// 1秒待機してrejectする(エラーの発生)
functionA();
$ node e_promise.js
THIS IS THE ERROR undefined?????
functionC()が発生させたエラーはfunctionB()の中でcatchされ、コンソールに出力されています。そこでエラーハンドリングは完了しており、
.catch()の中で値を返していませんので、functionA()には値が渡らず、undefinedになります。そして、そのundefinedを文字列として解釈して、それに"?????"をつけるなんていう、javascriptらしい珍妙な動きをしているわけです。
本当は
functionA()の中でちゃんとエラーをcatchしているので、functionB()のcatchは不要なのですが、.then()をつけたならその後ろに.catch(e=>console.log(e))をつけるという癖を付けておくと、エラーを完全スルーすることがなくなりますから、良いと思うんですよね。このあたり読者の皆さんどう思いますか。
次いきます。
Observableの場合のエラーハンドリング
e_observable.mjs
import rxjs from 'rxjs';
const { Observable } = rxjs;
import operators from 'rxjs/operators';
const { map } = operators;
const functionA = () => {
functionB().subscribe(x=>console.log(x+"?????"),e=>console.log(e));
}// エラーが流れてくるので、画面に表示。
// subscribe()の二つ目の引数は、エラーをハンドリングする関数。
const functionB = () => {
return functionC().pipe(map(x=>x+"!!!!!"));
}// functinoC()から来たObservableの中身に改造を施そうにも中身はエラーなので、そのままreturnして次へ流す。
const functionC = () => {
return Observable.create(observer=>{
setTimeout(()=>{
observer.error("THIS IS THE ERROR");
},1000);
});
}// 1秒待機してobserver.errorを呼ぶ(エラーの発生)
functionA();
$ node --experimental-modules e_observable.mjs
(node:17813) ExperimentalWarning: The ESM module loader is experimental. THIS IS THE ERROR
functionB()には、値を受け取る.subscribe()がありませんから、ここでエラーハンドリングはできません。functionA()の中で.subscribe()の二つ目の引数として、エラーハンドリング用の関数を与えています。.subscribe()の引数は、1つ目が値を受け取って処理する関数、2つ目がエラーを受け取って処理する関数を与えることになっています。
async/awaitの場合のエラーハンドリング
e_async.js
async function functionA(){
const x = await functionB();
console.log(x+"?????");
}// functionB()の結果を待機するが、エラーが来るので、エラーの入ったPromiseを作って次へ流す。
async function functionB(){
const x = await functionC();
return x + "!!!!!";
}// functionC()の結果を待機するが、エラーが来るので、エラーの入ったPromiseを作って次へ流す。
const functionC = () => {
return new Promise((resolve,reject)=>{
setTimeout(()=>{
reject("THIS IS THE ERROR");
},1000);
});
}// 1秒待機してrejectする(エラーの発生)
functionA().catch(e=>console.log(e));
// functionAがPromiseを返すので、ここでエラーハンドリングができる。(画面にTHIS IS THE ERRORが出力される)
$ node e_async.js
THIS IS THE ERROR
async/awaitを使った場合、.then().catch()を書く場所がありません。ではどうやってエラーハンドリングするかと言うと
async functionで宣言しているfunctionA()がPromiseを返しますから、そこまでの処理でエラーが発生していれば、ここまで伝播しています。
なので、この
functionA()の返り値に.catch()を付けてあげればいいですね。コードの最後に
functionA()を実行している所で、functionA().catch(e=>console.log(e));としてやればいいです。また、この記事では紹介していませんが、
try〜catchを使った方法もあります。
結論、3者で何が違った?
では最後に、改めて、3者で何が違ったか確認して、この記事を終わっておきましょう。
PromiseとObservableの作り方
new PromiseとObservable.create
//Promise
new Promise((resolve,reject)=>{
resolve("OH YEAH");
});
//Observable
Observable.create(observer=>{
observer.next("OH YEAH");
});
Observableを作る場合は、createメソッドに「observerを受け取る関数」を渡します。observerは
nexterrorcompleteという3つの関数を持つオブジェクトです。参考→RxJS基礎中の基礎
要は、いくつかの関数を受け取る関数を渡せば良いわけですね。
Promiseで言うところの
resolveと、Observableで言うところのnextまたはcompleteが似たような働きをします。エラーの場合は
rejectまたはerrorを使います。ただまあ、PromiseやObservableって、自分で作るケースよりも、何かしらのライブラリが返してきてそれを使うケースの方が多いと思うんですよね。
ですから、作り方の違いはあんまり重要じゃないかもしれません。むしろ以下の方が重要。
結果とエラーの受け取り方
.then()と.subscribe()とawait&最後にまとめてcatch
//Promise functionB().then(x=>console.log(x+"?????")).catch(e=>console.log(e)); //Observable functionB().subscribe(x=>console.log(x+"?????"),e=>console.log(e)); //async/await async関数の中で値を受け取る const x = await functionB(); console.log(x+"?????"); //async/await 最後にまとめてエラーをcatch functionA().catch(e=>console.log(e));
中身をいじって次にわたす方法
.then()と.pipe(map())とawait→return
//Promise return functionC().then(x=>x+"!!!!!"); //Observable return functionC().pipe(map(x=>x+"!!!!!")); //async/await async関数の中で const x = await functionC(); return x + "!!!!!";
ちなみにこの記事では触れてませんが、Promiseの場合のエラーハンドリング関数は、Observablelの
.subscribe()の時と同じように.then()の第二引数に渡してもいいですし、.catch().then()の順に書いてもいいです。
コメント
コメントを投稿