JavaScript
promise
async
RxJS
Observable

Promise・Observable・async/await のそれぞれで同じ事をやってみて書き方の違いを理解しよう

PromieObservableasync/awaitの3者って、似たような文脈でよく出てくるけど、出くわすたびになんとなく書いてなんとかしてた感があったので、

同じ動作をするコードを書いてみて、書き方にどんな違いが出てくるか比較してみよう、という趣旨の記事です。

きっと理解が深まると思います。


環境

node v8.9.4

rxjs 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!!!!!?????

1秒の待機の後、こちらが出力されました。

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();


実行してみます。(--experimental-modulesについてはここでは説明しないので気になる方はググってください)

$ node --experimental-modules observable.mjs

(node:17813) ExperimentalWarning: The ESM module loader is experimental.

OH YEAH!!!!!?????

1秒の待機の後、こちらが出力されました。さっきと全く同じですね。(いや、同じじゃない行も出てますが、nodeがmodulesに対応してない関係で出てるだけで、まあ、無視していいです。)

コードを見ると、

functionB()の中身が、.then()じゃなくて.pipe(map(())になってますね。

.then()は、Promiseの中身を受け取る時に使うメソッドですが、.pipe()はObservableの中身を受け取る時に使うメソッドではありません。

Observableの中身を受け取るときは.subscribe()を使いますからね。

.then()が一度Promiseの中身を受け取るのに対して、.pipe()はObservableの外から中身にだけ改造を施すようなイメージです。その改造に使うツールをoperatorと言って、.pipe()の引数に渡して使います。

map(x=>x+"!!!!!")というのは、「来たxx+"!!!!!"に改造する」ということですね。

で、.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!!!!!?????

1秒の待機の後、こちらが出力されました。こちらも全く同じです。

コードの見た目が結構変わりましたね。

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

今度は普通にTHIS IS THE ERRORが一回出ただけで終わりましたね。Warningの行は例によって無視してください。

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");
});

Promiseを作る場合は、コンストラクタに「(resolve,reject)という2つの関数のセットを受け取る関数」を渡します。

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())awaitreturn

//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()の順に書いてもいいです。