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ページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

回答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総合スコア775
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総合スコア18404
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/03/06 12:40

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総合スコア37411
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

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総合スコア117622
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。