どちらの記法がベストな記法なのか
混在させると悲惨な事になるので
プロジェクトの既存コードに合わせるのが大前提です。
ただし、プロジェクトを立ち上げる場面だとしましょう。
採用条件を選べるのなら絶対にasync/awaitの方を選びます。
ベースはasync/await、
要所要所でPromise流儀の書き方を強いられるので
必要最低限のみPromise流儀の書き方で解決するようにします。
何故そうなるのかに関しては歴史を紐解く必要があるので、そちらの解説からします。
まずはコールバック地獄のお話。
質問文で最初から「Promise使いたいです!」って言っているので恐らく既知の箇所と被ってるでしょう。
聞き流してやってください。
JavaScript(Node.js)はシングルスレッドです。
とりあえず書いてある事は一息で実行しようとします。
なのでネットワークやHDDへのアクセスは基本的には待ちません。
関数と達成条件をセットで渡してイベント登録を行い、
「イベントループ」という巡回している機能に代わりに関数を叩いてもらう設計になっています。
まぁ、これが原因で「コールバック地獄」になるんですがね。
JSがシングルスレッドであるがために既存のコードを一息で実行しようとして、
怠惰で後回しにするイベント登録という解決策しか用意されてない以上、
非同期処理を挟む時は、同期処理に帰って来れないのでif・for・try-catchといった制御構文が一切使えません。
(抽出しようと思っても値が帰ってくるまで待たず、持って帰れないので全ての制御構文がすり抜ける)
もし貴方の現場のメンバー全員が、
JavaScriptのルーツはLispなので、それのテクニックを流用してサラッと書けると思いますが、
普段からHaskellやLispで書きまくっている関数型オタクならいざ知らず
手続き型プログラミング言語のJavaを使っている人には相当無理がある要求です。
多分現場のコードはネストだらけの阿鼻叫喚のクソみたいなコードであふれかえるでしょう。
Promiseはその「コールバック地獄」を
オブジェクト指向プログラミングのテクニックで解決したものです。
PromiseインスタンスはES2016で正式にJSの仕様として採用されており、
Node.jsなら1系未満でも対応しています。
Promiseのインスタンスに対して.then(fn)
や.catch(fn)
メソッドを叩くだけなので
初期のJSのコールバック地獄から比べれば雲泥の差です。
しかし、所詮はオブジェクト指向プログラミングのテクニックでゴリ押ししただけ。
非同期処理を挟む時は、同期処理に帰って来れないのでif・for・try-catchといった制御構文が一切使えません。
この大問題は一生解決しません。(←これが言いたいからコールバック地獄の話した)
コードで説明しましょう。
js
1// このURL一覧にアクセスしてHTMLを持ち帰りたい
2const urls = [
3 "http://example.com/1",
4 "http://example.com/2",
5 "http://example.com/3",
6 "http://example.com/4",
7];
8
9// こう書けばよくね?
10Promise.all(
11 urls.map(url =>
12 axios.get(url).then(res => res.data)
13 )
14).then(results => {
15 console.log(results);
16});
17
18// → 同時接続しすぎなので一度に1通信だけにして欲しいと顧客に言われた
19// しゃあない、for文使って数珠つなぎにするか
20let promise = Promise.resolve(null);
21const results = [];
22for (const url of urls) {
23 promise = promise
24 .then(() => axios.get(url))
25 .then(res => results.push(res.data))
26 });
27}
28// 同期処理には帰ってこれないのでresultsの中身を見たければpromise.thenを叩くしか無い
29promise.then(() => {
30 console.log(results);
31});
なんか雲行きが怪しくなってきましたね。
あっ、これにエラー処理追加しないといけないですね。
js
1let promise = Promise.resolve(null);
2const results = [];
3for (const url of urls) {
4 promise = promise
5 .then(() => axios.get(url))
6 .then(res => results.push(res.data))
7 // ココに書くと失敗したURLを飛ばして次に行く
8 .catch(err => console.log(err));
9}
10promise
11 // 上記ではなくここだけに書くと、失敗したらそれ以降通信しない
12 .catch(err => console.log(err))
13 .then(results => console.log(results));
(一発書きなので多分動くと思いますが自信薄)
エラー処理挟むだけで2通りに分岐しましたね。
リーダーの貴方は部下が書いてきたこのコードを読んで、
「仕様と違うよね?直して」「仕様通りだね、さすが!」を判別しなければなりません。
私は毎日こんなコード読みたくありません。
可変長の配列のURLを順次アクセスするもっとスマートに書く方法があるかもしれないですが、
私のJS力だとちょっとこれが限界ですね。
私だったら「やりたいことに対するノイズ多すぎ、やってられるか!」と思いますね。
Promiseには限界があると思います。
んでようやくasync/await構文
これはES2017のPromiseの次に実装されたPromiseをよしなに操る糖衣構文です。
async関数を定義すると、必ずPromiseのインスタンスを返します。
await Promise
: .then(val => {})
に変換され、val
を同期処理っぽく取り出せる
return val
: resolve(val)
に変換される
try-catch
: .catch(中身)
に変換される
- if文: 謎の力でよしなにやってくれる!
- for文: 謎の力でよしなにやってくれる!
もうね、if,for,try-catchが非同期処理に混ぜて良いだけで神ですよ。
出るの1年おせえよES2016の時点で実装しろよ。
いやいや出てくれただけで神です。
js
1// async関数を定義しないとawait構文使えないからね
2const main = async () => {
3 const urls = [
4 "http://example.com/1",
5 "http://example.com/2",
6 "http://example.com/3",
7 "http://example.com/4",
8 ];
9
10 // 最初に書いたPromise.allを使うパターンも何気に恩恵受けてる
11 // Promise.allはエラー処理周りで融通効かないので質問文の条件を加味すると頻度は下がると思う
12 const results1 = await Promise.all(
13 urls.map(async url => {
14 const res = await axios.get(url);
15 console.log(res.data);
16 reutrn res.data;
17 })
18 );
19 console.log(results1);
20
21 // 途中でエラー吐いたら即停止したい
22 const results2 = [];
23 try {
24 for (const url of urls) {
25 const res = await axios.get(url);
26 console.log(res.data);
27 results2.push(res.data);
28 }
29 console.log(results2);
30 } catch (e) {
31 console.error(e);
32 }
33
34 // エラー吐いたURLは無視して次へ行きたい
35 const results3 = [];
36 for (const url of urls) {
37 try {
38 const res = await axios.get(url);
39 console.log(res.data);
40 results3.push(res.data);
41 } catch (e) {
42 console.error(e);
43 }
44 }
45 console.log(results3);
46};
47
48// 実行
49main();
50
51// こんな風にasyncのアロー関数を括弧で包んで即時実行しても良い
52(async () => {
53 // 内部の処理
54})();
内部的にPromiseを使いまくっているとはいえ、
Promise臭みたいなのはコードからほぼ消えました。
Promiseだけで全部やれ!の地獄を思えば雲泥の差です。
これなら流石に現場の人間、全員読めるし処理追えるでしょ……
まぁ、一部setTimeoutみたいなのはPromise使わないと上手く書けないので
そういう所は詳しい人がライブラリ化して別ファイルにしちゃいましょう。
そこだけ紹介して終わります。
js
1// setTimeoutを改造したsleep関数つくるよ
2const sleep = ms => new Promise(resolve =>
3 setTimeout(resolve, ms)
4);
5
6const main = async () => {
7 console.log(1);
8
9 // 3秒待ちたい
10 await sleep(3000);
11
12 console.log(2);
13};
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2021/10/06 00:58