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で作成しております。
ぜひ遊びに作業しに来てください!