質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
Node.js

Node.jsとはGoogleのV8 JavaScriptエンジンを使用しているサーバーサイドのイベント駆動型プログラムです。

TypeScript

TypeScriptは、マイクロソフトによって開発された フリーでオープンソースのプログラミング言語です。 TypeScriptは、JavaScriptの構文の拡張であるので、既存の JavaScriptのコードにわずかな修正を加えれば動作します。

Q&A

2回答

2374閲覧

asyncブロック内でmap関数内のawaitが使えない。

yuki_90453

総合スコア326

Node.js

Node.jsとはGoogleのV8 JavaScriptエンジンを使用しているサーバーサイドのイベント駆動型プログラムです。

TypeScript

TypeScriptは、マイクロソフトによって開発された フリーでオープンソースのプログラミング言語です。 TypeScriptは、JavaScriptの構文の拡張であるので、既存の JavaScriptのコードにわずかな修正を加えれば動作します。

0グッド

0クリップ

投稿2020/03/02 07:51

##概要
下記のようなasync関数内で、配列にmap関数で処理を行います。

export const handler=async(event: APIGatewayEvent, context:Context, callback:Callback)=>{ const array_1 = ["aaa","bb","cccc"] array_1.map((str:any)=>{ await updateProduct(str) }) }
export const updateProduct =async(str)=>{ const shopify = new Shopify2({ (省略) }) await shopify.product.update(str) .then((res:any)=>{ res }) .catch((res:any)=>{ res }) }

上記のawait式が「非同期関数内で使用出来ます」とコンパイルエラーになってしまいます。
handler関数でasync宣言しているのでmap内まで非同期が有効であると思うのでこのようなエラーになってしまい、困っております。

怪しい点などアドバイス頂けないでしょうか。
宜しくお願いします。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

問題点はシンプルで、async関数直下でしかawait構文は使えません。
入れ子OKにするとでかいasyncメイン関数内ならどこでも使い放題みたいになってコードが崩壊しますからね。
なのでArray.prototype.map(以下map)内でawaitを使いたければmapに引数もasync関数にしてやる必要があります。

しかしmapでasync関数を突っ込んでもPromiseの配列になるだけです。
なので単純にasync関数を突っ込むだけでは上手く行きません。
Promise.allを使いましょう。

私はTypeScriptに関してはあまり詳しくないのでJavaScriptで書きますが、
解説付きコードで表現するとこんな感じです。

js

1const handler = async (event, context, callback) => { 2 const array_1 = ["aaa","bb","cccc"] 3 4 // mapのコールバック関数もasync関数でくくってしまう。 5 const array_2 = array_1.map(async str => 6 await updateProduct(str) 7 ) 8 9 // Array.prototype.map内でasync関数作ると 10 // Promiseインスタンスの配列が払い出される 11 console.log(array_2) // [Promise, Promise, ...] 12 13 // もしupdateProductがPromiseを返すならこれでOK 14 // const array_2 = array_1.map(updateProduct) 15 16 // Promise.allにPromiseインスタンスの配列を突っ込めば 17 // 全ての実行を待って配列を取り出す一つのPromiseインスタンスになる 18 // そうすればawaitで待ちつつ配列形式で値を取り出せる 19 const array_result = await Promise.all(array_2) 20}

ですが、そもそもupdateProductという名称から目的を推測するに、
非同期処理を伴う戻り値Promiseインスタンスですよね?
もし違うならawaitで待ちたいとかお前は何を言っているんだ?みたいな状態になってしまいますね。

上記を踏まえてシンプルに書くと、
最終形はこんな感じになっていると考えられます。

js

1const handler = async (event, context, callback) => { 2 const array_1 = ["aaa","bb","cccc"] 3 const result = await Promise.all( 4 array_1.map(updateProduct) 5 ) 6}

ただ外部のAPIと通信するのでPromise.allでは並列で1度にAPIを叩いてしまう処理になってしまいlimitを超えてしまう為、使用を控えております。

mapというのは値を関数に従って変形させた結果を取り出すという目的に使われるものなので、
高度な非同期処理を内包したものを作るには不向きです。
なので、基本的にはfor文でお行儀よく1個ずつ処理を走らせる方向で考えるのが自然だと思います。

それでもリスト操作でこなすなら
100個のリクエストというボールが入ったプールがあったとして、
バケツで5個ずつ組み上げる作戦があります。

Lodashのchunk関数が丁度それに使えます。
実装をコードになおすと下記のような感じ。

js

1const range = (a, b) => Array(b - a + 1).fill(0).map((_, i) => i + a); 2console.log(range(1, 10)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 3 4const chunk = (arr, size) => arr.reduce((result, it, i) => { 5 if (!result[Math.floor(i / size)]) result[Math.floor(i / size)] = []; 6 result[Math.floor(i / size)].push(it); 7 return result; 8}, []); 9console.log(JSON.stringify(chunk(range(1, 21), 5), null, 2));

json

1[ 2 [ 3 1, 4 2, 5 3, 6 4, 7 5 8 ], 9 [ 10 6, 11 7, 12 8, 13 9, 14 10 15 ], 16 [ 17 11, 18 12, 19 13, 20 14, 21 15 22 ], 23 [ 24 16, 25 17, 26 18, 27 19, 28 20 29 ], 30 [ 31 21 32 ] 33]

これを実際に作るならreduceを使いながらthenを次々に転がすような実装になります。

js

1const chunk = (arr, size) => arr.reduce((result, it, i) => { 2 if (!result[Math.floor(i / size)]) result[Math.floor(i / size)] = [] 3 result[Math.floor(i / size)].push(it) 4 return result 5}, []) 6 7const handler = async (event, context, callback) => { 8 const array_1 = ["aaa","bb","cccc"] 9 const limit = 5 10 await chunk(array_1, limit).reduce((promise, arr) => { 11 promise.then(() => Promise.all( 12 arr.map(updateProduct) 13 )) 14 return promise 15 }, Promise.resolve(1)) 16}

まぁ、chunkのアイデアまでは良いと思いますが、
reduceは明らかにES5まで退行しているので、
素直にfor...ofを使った方が100倍見栄えは良いでしょう。

js

1const chunk = (arr, size) => arr.reduce((result, it, i) => { 2 if (!result[Math.floor(i / size)]) result[Math.floor(i / size)] = [] 3 result[Math.floor(i / size)].push(it) 4 return result 5}, []) 6 7const handler = async (event, context, callback) => { 8 const array_1 = ["aaa","bb","cccc"] 9 const limit = 5 10 for (const arr of chunk(array_1, limit)) { 11 await Promise.all( 12 arr.map(updateProduct) 13 ) 14 } 15}

最速で行きたいなら
stateを使って状態管理した方が良いでしょうね。
状態管理に依存するので使うとするならforEach1個だけで頑張るのが無難ですが、
だったらやはりfor...ofでよくね?と感じます。

まぁ、Babelはasync関数とfor...ofの同時利用を解決出来るか怪しいので、
forEachで書けそうなら頑張っても良いと思いますが。

js

1const waitFor = (ms) => new Promise(resolve) => 2const waitUpdate = async (state, limit) => { 3 while (true) { 4 if (state.map(it => it.loading).length < limit) break 5 await waitFor(1000) // リミット件数実行中なら1秒程度待つ 6 } 7} 8 9const handler = async (event, context, callback) => { 10 const array_1 = ["aaa","bb","cccc"] 11 const limit = 5 12 const states = array_1.map(str => ({str, loading: false})) 13 for (const state of states) { 14 await waitUpdate(states, limit) 15 state.loading = true 16 updateProduct(state.str).then(() => state.loading = false) 17 } 18}

大体こんな感じがゴールになるでしょう。

投稿2020/03/02 09:21

編集2020/03/05 12:55
miyabi-sun

総合スコア21158

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

yuki_90453

2020/03/05 06:03

詳細の回答ありがとうございます。 いくつか質問お願い致します。 >しかしmapでasync関数を突っ込んでもPromiseの配列になるだけです。 以前から気になっていたのですがPromiseを用いた処理で出力されたPromise〜とは一般的なstringやnumberとは型が出力されるのでしょうか? >ですが、そもそもupdateProductという名称から目的を推測するに、 >非同期処理を伴う戻り値Promiseインスタンスですよね? 仰る通りです。ただ外部のAPIと通信するのでPromise.allでは並列で1度にAPIを叩いてしまう処理になってしまいlimitを超えてしまう為、使用を控えております。 やはりmapなどで処理せずブロック内でawaitを使えるforで書いた方が、良いのでしょうか。 宜しくお願いします
miyabi-sun

2020/03/05 12:07

前者はPromiseインスタンスはPromiseオブジェクトをnewして作られたインスタンスです。 TypeScriptでPromiseインスタンスからthenメソッドを叩いて得られる引数の型の話なら、 制御しにくいとは思いますが、私はTypeScriptを触ってないのでその辺は別途勉強してください。 後者に関しては完全に後出しですが、追記しておきます。
guest

0

handler関数でasync宣言しているのでmap内まで非同期が有効であると思うので

残念ながら、awaitasync関数内に直接書くことしかできません。

「コールバックを要さないfor系の構文で回しながらawaitを行う(1個ずつ実行される)」もしくは「全部をmapしてPromiseの配列に変換した上で、await Promise.all(配列)とする(全部並列実行)」などの手段が必要となります。

投稿2020/03/02 09:06

maisumakun

総合スコア145121

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問