LoginSignup
80
53

More than 1 year has passed since last update.

Promise・Observable・async/await で同じ事をやってみて違いを理解しよう

Last updated at Posted at 2019-01-24

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

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

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

3者の関係を説明しておくと、

  • Promiseの扱い方の中に、thenを使う方法とasync/awaitを使う方法がある
  • Observableの扱いは、Promiseの「thenを使う方法」と似ている

という感じです。

thenasync/awaitはどちらもPromiseを扱う方法で、PromiseObservableは似ていますが違うものです。

(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の場合

ではコードを見ていきましょう。

a_promise.js
// 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秒の待機の後、こちらが出力されました。

functionBfunctionAによる修飾(!!!!!と?????を付け足す)がちゃんと機能してますね。

見ておきたいポイントはfunctionBです。

一度thenで値を取り出した後、thenに渡した関数の中でx+"!!!!!"returnしています。

(アロー関数になっていてreturnという文字はないですが、=>の右側がreturnされる中身です。)

そしてそのreturnされたモノを中身として持つPromiseが、thenの返り値になります

returnされた中身を改めてPromiseで包んで、全体(functionB)の返り値にしてるというイメージです。

では次。

Observableの場合

コードを見てみます。

注意:nodeで実行するための記述がいくつかあります。本筋からそれるのでここでは解説を省略します。

  • ファイル名の拡張子が.mjsなこと
  • import文
  • 実行コマンドのオプション--experimental-modules
  • 出力の一行目

npm install rxjsは済ませてあります。

a_observable.mjs
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+"!!!!!")は、「来たxx+"!!!!!"に改造する」というoperatorです。

で、pipeの返り値はまたObservableになります。

ちなみにpipeの中には複数のoperatorを入れることができ、書いた順に適用されます。

async/awaitの場合

a_async.js
// 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の場合のエラーハンドリング

e_promise.js
// 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の場合のエラーハンドリング

e_observable.mjs
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の場合のエラーハンドリング

e_async.js
// 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()) + "?????");

Promisethenメソッドに、Observablesubscribeメソッドに、中身を受け取る関数を渡すことができます。

さらに、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で作成しております。

みんなで作業場

ぜひ遊びに作業しに来てください!

80
53
2

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
80
53