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

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

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

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

Q&A

解決済

4回答

10647閲覧

javascript 複数のPromise.allをそれぞれのPromiseが完了してから実行したい

navca

総合スコア44

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

1グッド

4クリップ

投稿2019/03/05 23:05

async/awaitの勉強のためにpromiseを勉強しています。

やりたいことは、要素が3つずつある2次元の配列([[1,2,3],[4,5,6],...])をループする形で、一つめの配列をpromise.allで並列実行し、それがおわれば次の配列をpromise.allで実行...という風にしたいです。
それを実現するとなると以下の様な構造でやるしかないでしょうか?

Promise.all([1,2,3]) .then(Promise.all([4,5,6]) .then(Promise.all([7,8,9]) .then(Promise.all([10,11,12]))))

以下に試しに書いてみましたが、4秒ぐらいですべての処理が終わってしまいます。本当は9秒で終えてほしいです。setTimeout(3秒) * 3

function main(n){ return new Promise(resolve => { setTimeout(console.log, 3000, 'インデックス' + n + 'の処理終了') resolve() }) } function promisewrapper(arr){ return Promise.all(arr) } let array = [1,2,3,4,5,6,7,8,9] function divide(arr, n){ return arr.reduce((new_arr, _ , i)=> i % n === 0 ? [...new_arr, arr.slice(i, i + n)] : new_arr, []) } array = divide(array, 3) array = array.map(val => val.map(val2 => main(val2))) array = array.map((x, i) => promisewrapper(x, i)) array.reduce((pre, cur, i) => pre.then(() => {cur; return cur}), Promise.resolve())

チェーン化する場合、pre.thenのcallback内でreturn preすると思いますが、return curとしましたが、結果が変わりませんでした。

退会済みユーザー👍を押しています

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

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

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

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

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

Lhankor_Mhy

2019/03/06 01:05

とりあえず、main関数の resolve() が setTimeout関数の外にあるので、これはノータイムで次のチェーンに行ってしまいませんかね?
guest

回答4

0

ベストアンサー

まとめて処理されてしまう原因は、

js

1array = array.map(val => val.map(val2 => main(val2)))

この加工でmainを実行しているためです。

なぜそのようなコーディングをされたのかを想像しましたところ、
Promiseを少し勘違いされているのかなと思いました。

下記コードはどんな挙動をされると思いますか?

js

1new Promise(resolve=>{ 2 console.log("test"); 3})

navcaさんの予想ではtestは出力されない、ということになっていませんか?
実際の挙動は、Promiseの引数となっている関数は即時実行されます。

実際にnavcaさんのコードに若干手を加えて、setTimeoutの前にログを出してみましょう。

function main(n){ return new Promise(resolve => { console.log(`インデックス${n}の処理開始`) // 追記 //setTimeoutの使い方が間違えていらっしゃったので修正しました。 //setTimeoutのコールバック関数の中にresolveを実行させ、Promiseの正常終了を伝えます。 setTimeout((msg)=> { console.log(msg) resolve() }, 3000, `インデックス${n}の処理終了`) }) } function promisewrapper(arr){ return Promise.all(arr) } let array = [1,2,3,4,5,6,7,8,9] function divide(arr, n){ return arr.reduce((new_arr, _ , i)=> i % n === 0 ? [...new_arr, arr.slice(i, i + n)] : new_arr, []) } array = divide(array, 3) array = array.map(val => val.map(val2 => main(val2))) //ここまで!あえてpromisewrapperに加工している処理と、reduceループさせている処理を省いてます。

navcaさんの想像とは違う挙動になっていると思います。
console.logが実行され、setTimeoutも実行されているのが確認できたと思います。
恐らく、navcaさんはPromiseの引数はcallback関数のイメージだったのではないでしょうか。

Promiseは処理の終了を教えてくれる、resolve,rejectされるまで待つ、という仕組みの1パーツなだけでして、遅延評価される関数を包み込んだオブジェクト?(言葉の表現が下手ですみません)ではありません。この勘違いがなくなれば、きっとreduceを使ったPromiseなループ処理も怖くなくなると思います。

理解の助けになるように、navcaさんの関数定義をそのまま使った場合で修正しますと・・・

js

1function main(n){ 2 return new Promise(resolve => 3 { 4 setTimeout((n)=>{ 5 console.log(`インデックス${n}の処理終了`) 6 resolve(n) 7 }, 3000, n) 8 }) 9} 10 11function promisewrapper(arr){ 12 //このタイミングでPromiseな配列に加工して 13 arr = arr.map(val => main(val)) 14 //Promise.allに任せる 15 return Promise.all(arr) 16} 17 18let array = [1,2,3,4,5,6,7,8,9] 19 20function divide(arr, n){ 21 return arr.reduce((new_arr, _ , i)=> i % n === 0 ? [...new_arr, arr.slice(i, i + n)] : new_arr, []) 22} 23 24array = divide(array, 3) 25 26array.reduce( (acc, cur) => { 27 return acc.then( () => promisewrapper(cur) ) //Promise.allの結果待ち、要するにここで非同期な処理であるmainが実行されるように書くこと 28}, Promise.resolve()) 29

感覚的にはpromisewrapperを外した↓のほうがわかりやすいでしょう

js

1array.reduce( (acc, cur) => { 2 return acc.then( () => { 3 //このタイミングでPromiseな配列に加工して 4 cur = cur.map(val => main(val)) 5 //Promise.allに任せる 6 return Promise.all(cur) 7 } ) 8}, Promise.resolve() )

投稿2019/03/06 03:04

編集2019/03/06 03:12
so87

総合スコア764

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

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

navca

2019/03/06 13:18

みなさんご回答ありがとうございます。たくさん例を挙げていただいたので後日ゆっくり理解したいと思います。ありがとうございました。
navca

2019/03/09 14:21

ご回答ありがとうございました。 function promisewrapper(arr){ return Promise.all(arr) } ↑はpromise.allの実行結果を返すということに気づけなかったのは不覚でした!promise.allの実行タイミングもなるほどでした。ありがとうございました。
guest

0

配列のmapなどで処理を書く前にもっと注意深くご自分のコードを検証(例えば実際に動かして)してみるべきだと思います。
なおここでは「メッセージがいつ表示されたか」は重要な情報です。以下の例をまず動かしてみてください。

js

1const start = +new Date() 2 3function log(...m) { // メッセージを時刻とともに表示するデバッグ用関数 4 let t = '' + (+new Date() - start) 5 t = '0'.repeat(Math.max(0, 6 - t.length)) + t 6 console.log(`${t}:`, ...m) 7} 8 9function main(n) { 10 return new Promise(resolve => { 11 log(`インデックス${n}の起動`) 12 setTimeout(() => { 13 log(`インデックス${n}の処理終了`) 14 }, 3000) 15 { 16 log(`resolved ${n}`) 17 resolve() 18 } 19 }) 20} 21 22main(1) 23.then(() => main(2)) 24.then(() => log('completed'))

000000: インデックス1の起動
000002: resolved 1
000003: インデックス2の起動
000003: resolved 2
000003: completed
003014: インデックス1の処理終了
003014: インデックス2の処理終了

ここから2つの問題に気づけます。

問題1: 処理が完了する前にresolveしてはいけない

Lhankor_Mhyさん指摘のとおりです。処理が完了する前にresolveしてしまっていることに気づけてないようです。main関数は次のようになってなければそもそも意味がありません。

js

1function main(n) { 2 return new Promise(resolve => { 3 log(`インデックス${n}の起動`) 4 setTimeout(() => { 5 log(`インデックス${n}の処理終了`) 6 resolve() 7 }, 3000) 8 // ここ以降に何か書いても「即座に」実行されてしまう 9 }) 10}

このように修正して最初のコードを実行すると

000001: インデックス1の起動
003018: インデックス1の処理終了
003020: インデックス2の起動
006032: インデックス2の処理終了
006032: completed

のような結果になります。まずはこのような結果になるmainを正しく実装した上でスタートしないことには話が始まりません。

問題2: new Promiseをすると非同期処理が起動されてしまう

元のコードにある
array.map(val => val.map(val2 => main(val2)))
を実行した途端無条件に9つの非同期処理は全て起動されるのです。9つが一斉に起動されるため完了もまた3秒後ぐらいになるのは明らかです。この大前提を把握できてないとそもそもそれ以降のPromise.allなどの意味がないわけです。

チェーン化する場合、pre.thenのcallback内でreturn preすると思いますが、return curとしましたが、結果が変わりませんでした。

この疑問自体、問題2を認識しておられないためのあい路にはまっているといえましょう。ここでpre/curのどちらを返すかということは問題の本質ではないのです。

問題2からmapやreduceで直接new PromiseやPromise.allしてはならないことがわかります。map/reduceなどの関数は「ある要素がPromiseを生成したらそれが完了するまで次の要素の処理を待ってくれる」なんて機能はなく無条件にどんどん要素を処理していくのですから。

さて、

js

1function concurrentMain(ns) { 2 return Promise.all(ns.map(n => main(n))) 3}

という関数を仮定すると

例1:

js

1concurrentMain([1, 2, 3]) 2 .then(() => concurrentMain([4, 5, 6])) 3 .then(() => concurrentMain([7, 8, 9])) 4 .then(() => log('completed'))

とやれば任意の非同期処理の並列な実行とシリアルな実行の組み合わせが可能です。ここでも
.then(concurrentMain([...]))
ではなく
.then(() => concurrentMain([...]))
としなければならない点に注意してください。thenの引数は前段の非同期処理が完了する以前に評価されますので直接concurrentMainを呼び出してしまうと意味がなくなります。thenには「非同期処理が完了したときに実行したい関数」を指定するようにしてください。

ご質問の最初の段階のthenチェーンの例はPromise.allの引数の実際の書き方を省略しているおつもりなのでしょうが、その書き方にこそ重要な前提知識があるわけで、そこは省略せずに書いて実際に動かしてから質問を発した方がよかったと思います。

js

1Promise.all([1,2,3]) 2 .then(Promise.all([4,5,6]) 3 .then(Promise.all([7,8,9]) 4 .then(Promise.all([10,11,12])))) 5// => 本来は例1のように書くべき

ご質問に対する対策案

ここまでのことを踏まえて最初のご質問に戻りましょう。

それを実現するとなると以下の様な構造でやるしかないでしょうか?

つまりthenチェーンで書かねばならないのかということですが...「任意の段数を共通的な論理で書きたい」というならmainやconcurrentMainを直接呼び出すのではなく、それを行うような関数を生成しておいて、最後にその関数を起動する手もあると思います。結局はthenチェーンを使うのですが、thenチェーンを直接記述せずthenチェーンを行う関数呼び出しを生成するという考え方です。

js

1... 2 3let array = [1, 2, 3, ..., 8, 9] 4array = divide(array, 3) 5 6cf = array.reduce( 7 (pre, cur) => () => pre().then(() => concurrentMain(cur)), 8 () => Promise.resolve()) 9 10cf()

reduceに与える関数の引数は以下のように考えます。
・pre: 前段の非同期処理を表す「Promiseを返すような関数」
・cur: 非同期処理を起動する材料(パラメーター)

実行結果:
000000: インデックス1の起動
000002: インデックス2の起動
000003: インデックス3の起動
003018: インデックス1の処理終了
003019: インデックス2の処理終了
003019: インデックス3の処理終了
003020: インデックス4の起動
003021: インデックス5の起動
003021: インデックス6の起動
006032: インデックス4の処理終了
006032: インデックス5の処理終了
006032: インデックス6の処理終了
006033: インデックス7の起動
006033: インデックス8の起動
006033: インデックス9の起動
009047: インデックス7の処理終了
009047: インデックス8の処理終了
009047: インデックス9の処理終了
009047: completed

なんだか並列な実行を順番に動かすのに「関数を生成する」のがややこしく感じます。こういうややこしさはasync/awaitを使うと緩和できます。

js

1... 2 3async function run(array) { 4 for (let ns of array) { 5 await concurrentMain(ns) 6 } 7 log('completed') 8} 9 10run(array)

ここでArray.prototype.mapではなくfor文を使っていることに注意してください。先に述べたようにmapは「要素の実行が非同期関数の呼び出しかどうか」「非同期関数の呼び出しなら完了までまつ」なんて配慮を一切しない関数ですのでfor文を使ってかかねばならないのです。「for文書かないとだめなのかぁ」とげっそりするかも知れませんが、map的な高階関数を使いたい場合、非同期関数を同期的に適用してくれるような特別な関数でなければなりません。そういうmap/forEach関数は今のところ標準のArrayにはないと思います。

js

1... 2 3Array.prototype.asyncForEach = async function (op) { 4 for (let i = 0; i < this.length; i++) { 5 if (i in this) { 6 await op(this[i], i) 7 } 8 } 9} 10 11async function run(array) { 12 array.asyncForEach(async funcion(ns) { await concurrentMain(ns) }) 13 log('completed') 14} 15 16run(array)

無理やり書くならこんな感じになってしまうでしょう。
追記: スミマセン。最初の回答に書いたasyncMapはmapとは言えませんし定義も不適切でした。単に繰り返しをするだけのものなので関数名をasyncForEachに変更し実装もそれなりにしました。なおオリジナルのforEachに比べ仕様を簡略化しています。)

投稿2019/03/06 04:18

編集2019/03/06 12:00
KSwordOfHaste

総合スコア18392

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

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

KSwordOfHaste

2019/03/06 04:22

ながなが回答を書いているうちに他の方の回答と大分かぶった内容になってしまいました。async/awaitを学ぶためにということからその点にも少し言及したので、かぶった内容ではありますがそのまま投稿させていただきました。読みづらいかと思いますがご容赦ください。
KSwordOfHaste

2019/03/06 12:02

回答中に色々間違いがありました。特にasyncMapはmapとは言えない関数になってしまっており実装も変でした。気づいた範囲で訂正させていただきました。
Lhankor_Mhy

2019/03/06 12:40

Async Iteration と for await ... of の実装が待たれますね。
navca

2019/03/06 13:18

みなさんご回答ありがとうございます。たくさん例を挙げていただいたので後日ゆっくり理解したいと思います。ありがとうございました。
navca

2019/03/09 14:21

改めて回答ありがとうございました。 >配列のmapなどで処理を書く前にもっと注意深くご自分のコードを検証(例えば実際に動かして)してみるべきだと思います。 耳の痛い話です。非同期の実行のされ方に目が行ってしまい、array.map(val => val.map(val2 => main(val2)))この時点ですでにpromiseが実行されていたたことは全く目に入りませんでした。。。 >処理が完了する前にresolveしてはいけない これは非常に混乱したところでした。Promiseの引数内の個々の処理が非同期に行われるのですね。引き数にいれた関数自体が非同期に実行され、個々の処理は同期的に処理されると勘違いしていたかと思います。 async/awaitの例もありがとうございました。参考にさせていただきます。
guest

0

関数の引数は関数実行時に評価されます。
.then()の引数も例外ではありません。.then()が実行するタイミング、つまりPromiseを返すタイミングで評価されます。

js

1function main(n){ 2 return new Promise(resolve => { 3 setTimeout(x=>{console.log(x);resolve()}, 3000, 'インデックス' + n + 'の処理終了') 4 }) 5} 6Promise.all([main(1),main(2),main(3)]) 7 .then((()=>{ 8 console.log('then()が引数を評価してPromiseを返した'); 9 return ()=>{ 10 console.log('then()がresolveを受けた'); 11 return Promise.all([main(4),main(5),main(6)]); 12 } 13 })())

なので、.then(Promise.all([4,5,6]))のように書くと、Primise.all()は即座に評価され、Primise.allの引数が即座に評価され、配列の関数が実行されるので、全ての関数がほぼ同時に解決を始めます。

二つ目のコードで言うと、

js

1function promisewrapper(arr){ 2 return Promise.all(arr) 3}

とありますから、promisewrapperが呼ばれる=戻り値が評価される=Promise.all(arr)が評価されます。ですから、array = array.map((x, i) => promisewrapper(x, i))の時点で解決が始まってしまいます。戻り値を評価して欲しくないなら関数でラップして返すしかないでしょう。

投稿2019/03/06 01:31

編集2019/03/06 01:50
Lhankor_Mhy

総合スコア35860

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

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

navca

2019/03/06 13:18

みなさんご回答ありがとうございます。たくさん例を挙げていただいたので後日ゆっくり理解したいと思います。ありがとうございました。
navca

2019/03/09 14:33

回答ありがとうございました。 頂いたコードを実行したうえで疑問があります。 以下のURLには、>用意した3つのインスタンスをall()で実行します。全てのインスタンスの状態が履行(fulfilled)になった時点で処理が完了し、then()の第1引数が実行されます。  とあります https://lab.syncer.jp/Web/JavaScript/Reference/Global_Object/Promise/all/ だとしたら、↓こういう順序で出力されないとおかしいと思うのですが、 インデックス1の処理終了 インデックス2の処理終了 インデックス3の処理終了 then()が引数を評価してPromiseを返した 実際はこうなのはなぜなんでしょうか?↓ then()が引数を評価してPromiseを返した インデックス1の処理終了 インデックス2の処理終了 インデックス3の処理終了 お手数ですがご回答いただければ幸いです。
navca

2019/03/09 14:37

すいません。わかったかもしれません。thenの中でreturnされたものがpromiseである場合にその処理が非同期に実行され、そうでないものは即実行されると考えたら納得いきました。
Lhankor_Mhy

2019/03/11 00:18

「then()の第1引数が実行され」は「then()の第1引数の評価値が実行され」という意味ですね。 このコードの場合、第1引数が即時関数なので。
guest

0

配列をどうmainにわたしているのか書かれていませんね
フローでいうとこんな感じです

javascript

1Promise.all([ 2 new Promise(function(resolve){setTimeout(function(){console.log(1);return resolve()},1000);}), 3 new Promise(function(resolve){setTimeout(function(){console.log(2);return resolve()},0);}), 4 new Promise(function(resolve){setTimeout(function(){console.log(3);return resolve()},500);}), 5 ]).then(function(){ 6 console.log('step 1'); 7 Promise.all([ 8 new Promise(function(resolve){setTimeout(function(){console.log(4);return resolve()},0);}), 9 new Promise(function(resolve){setTimeout(function(){console.log(5);return resolve()},1000);}), 10 new Promise(function(resolve){setTimeout(function(){console.log(6);return resolve()},500);}), 11 ]).then(function(){ 12 console.log('step 2'); 13 Promise.all([ 14 new Promise(function(resolve){setTimeout(function(){console.log(7);return resolve()},500);}), 15 new Promise(function(resolve){setTimeout(function(){console.log(8);return resolve()},1000);}), 16 new Promise(function(resolve){setTimeout(function(){console.log(9);return resolve()},0);}), 17 ]).then(function(){ 18 console.log('end'); 19 }); 20 }); 21 }); 22

まとめるとこう

javascript

1const plist=[ 2 {text:"1",wait:1000}, 3 {text:"2",wait:0}, 4 {text:"3",wait:500}, 5 {text:"4",wait:0}, 6 {text:"5",wait:1000}, 7 {text:"6",wait:500}, 8 {text:"7",wait:500}, 9 {text:"8",wait:1000}, 10 {text:"9",wait:0}, 11 ]; 12const p=n=>new Promise((resolve)=>{ 13 setTimeout(()=>{ 14 console.log(plist[n].text); 15 return resolve(); 16 },plist[n].wait); 17}); 18Promise.all([p(0),p(1),p(2)]).then(()=>{ 19 console.log('step 1'); 20 Promise.all([p(3),p(4),p(5)]).then(()=>{ 21 console.log('step 2'); 22 Promise.all([p(6),p(7),p(8)]).then(()=>{ 23 console.log('end'); 24 }); 25 }); 26}); 27

参考

javascript

1const plist=[ 2 {text:"1",wait:1000}, 3 {text:"2",wait:0}, 4 {text:"3",wait:500}, 5 {text:"4",wait:0}, 6 {text:"5",wait:1000}, 7 {text:"6",wait:500}, 8 {text:"7",wait:500}, 9 {text:"8",wait:1000}, 10 {text:"9",wait:0}, 11 ]; 12const p=n=>new Promise((resolve)=>{ 13 setTimeout(()=>{ 14 console.log(plist[n].text); 15 return resolve(); 16 },plist[n].wait); 17}); 18(async ()=>{ 19 await (async (...args)=>{await Promise.all(args.map(arg=>p(arg)));})(0,1,2); 20 console.log("step 1"); 21 await (async (...args)=>{await Promise.all(args.map(arg=>p(arg)));})(3,4,5); 22 console.log("step 2"); 23 await (async (...args)=>{await Promise.all(args.map(arg=>p(arg)));})(6,7,8); 24 console.log("end"); 25})();

投稿2019/03/06 01:08

編集2019/03/06 02:37
yambejp

総合スコア114572

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

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

navca

2019/03/06 13:18

みなさんご回答ありがとうございます。たくさん例を挙げていただいたので後日ゆっくり理解したいと思います。ありがとうございました。
navca

2019/03/09 14:21

読ませていただきました。async/awaitの例ありがとうございました。参考にします。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.50%

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

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

質問する

関連した質問