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

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

ただいまの
回答率

90.45%

  • JavaScript

    21067questions

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

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

解決済

回答 4

投稿

  • 評価
  • クリップ 3
  • VIEW 756

navca

score 33

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としましたが、結果が変わりませんでした。

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

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • Lhankor_Mhy

    2019/03/06 10:05

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

    キャンセル

回答 4

checkベストアンサー

+5

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

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


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

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

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

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


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さんの関数定義をそのまま使った場合で修正しますと・・・

function main(n){
    return new Promise(resolve =>
    {
        setTimeout((n)=>{
          console.log(`インデックス${n}の処理終了`)
        resolve(n)
      }, 3000, n)
       })
}

function promisewrapper(arr){
    //このタイミングでPromiseな配列に加工して
    arr = arr.map(val => main(val))
    //Promise.allに任せる
    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.reduce( (acc, cur) => {
  return acc.then( () => promisewrapper(cur) ) //Promise.allの結果待ち、要するにここで非同期な処理であるmainが実行されるように書くこと
}, Promise.resolve())

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

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

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/03/06 22:18

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

    キャンセル

  • 2019/03/09 23:21

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

    キャンセル

+5

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

const start = +new Date()

function log(...m) { // メッセージを時刻とともに表示するデバッグ用関数
  let t = '' + (+new Date() - start)
  t = '0'.repeat(Math.max(0, 6 - t.length)) + t
  console.log(`${t}:`, ...m)
}

function main(n) {
  return new Promise(resolve => {
    log(`インデックス${n}の起動`)
    setTimeout(() => {
      log(`インデックス${n}の処理終了`)
    }, 3000)
    {
      log(`resolved ${n}`)
      resolve()
    }
  })
}

main(1)
.then(() => main(2))
.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関数は次のようになってなければそもそも意味がありません。

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


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

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を生成したらそれが完了するまで次の要素の処理を待ってくれる」なんて機能はなく無条件にどんどん要素を処理していくのですから。

さて、

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


という関数を仮定すると

例1:

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


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

ご質問の最初の段階のthenチェーンの例は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]))))
// => 本来は例1のように書くべき

ご質問に対する対策案

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

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

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

...

let array = [1, 2, 3, ..., 8, 9]
array = divide(array, 3)

cf = array.reduce(
  (pre, cur) => () => pre().then(() => concurrentMain(cur)),
  () => Promise.resolve())

cf()


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を使うと緩和できます。

...

async function run(array) {
  for (let ns of array) {
    await concurrentMain(ns)
  }
  log('completed')
}

run(array)


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

...

Array.prototype.asyncForEach = async function (op) {
  for (let i = 0; i < this.length; i++) {
    if (i in this) {
      await op(this[i], i)
    }
  }
}

async function run(array) {
  array.asyncForEach(async funcion(ns) { await concurrentMain(ns) })
  log('completed')
}

run(array)


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

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/03/06 13:22

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

    キャンセル

  • 2019/03/06 21:02

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

    キャンセル

  • 2019/03/06 21:40

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

    キャンセル

  • 2019/03/06 22:18

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

    キャンセル

  • 2019/03/09 23:21

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

    キャンセル

+3

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

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


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

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

function promisewrapper(arr){
    return Promise.all(arr)
}


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

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/03/06 22:18

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

    キャンセル

  • 2019/03/09 23: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の処理終了
    お手数ですがご回答いただければ幸いです。

    キャンセル

  • 2019/03/09 23:37

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

    キャンセル

  • 2019/03/11 09:18

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

    キャンセル

+2

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

Promise.all([
  new Promise(function(resolve){setTimeout(function(){console.log(1);return resolve()},1000);}),
  new Promise(function(resolve){setTimeout(function(){console.log(2);return resolve()},0);}),
  new Promise(function(resolve){setTimeout(function(){console.log(3);return resolve()},500);}),
  ]).then(function(){
    console.log('step 1');
    Promise.all([
      new Promise(function(resolve){setTimeout(function(){console.log(4);return resolve()},0);}),
      new Promise(function(resolve){setTimeout(function(){console.log(5);return resolve()},1000);}),
      new Promise(function(resolve){setTimeout(function(){console.log(6);return resolve()},500);}),
      ]).then(function(){
        console.log('step 2');
        Promise.all([
          new Promise(function(resolve){setTimeout(function(){console.log(7);return resolve()},500);}),
          new Promise(function(resolve){setTimeout(function(){console.log(8);return resolve()},1000);}),
          new Promise(function(resolve){setTimeout(function(){console.log(9);return resolve()},0);}),
          ]).then(function(){
            console.log('end');
          });
      });
  });

まとめるとこう

const plist=[
  {text:"1",wait:1000},
  {text:"2",wait:0},
  {text:"3",wait:500},
  {text:"4",wait:0},
  {text:"5",wait:1000},
  {text:"6",wait:500},
  {text:"7",wait:500},
  {text:"8",wait:1000},
  {text:"9",wait:0},
  ];
const p=n=>new Promise((resolve)=>{
  setTimeout(()=>{
    console.log(plist[n].text);
    return resolve();
  },plist[n].wait);
});
Promise.all([p(0),p(1),p(2)]).then(()=>{
    console.log('step 1');
    Promise.all([p(3),p(4),p(5)]).then(()=>{
      console.log('step 2');
      Promise.all([p(6),p(7),p(8)]).then(()=>{
        console.log('end');
      });
    });
});

参考

const plist=[
  {text:"1",wait:1000},
  {text:"2",wait:0},
  {text:"3",wait:500},
  {text:"4",wait:0},
  {text:"5",wait:1000},
  {text:"6",wait:500},
  {text:"7",wait:500},
  {text:"8",wait:1000},
  {text:"9",wait:0},
  ];
const p=n=>new Promise((resolve)=>{
  setTimeout(()=>{
    console.log(plist[n].text);
    return resolve();
  },plist[n].wait);
});
(async ()=>{
  await (async (...args)=>{await Promise.all(args.map(arg=>p(arg)));})(0,1,2);
  console.log("step 1");
  await (async (...args)=>{await Promise.all(args.map(arg=>p(arg)));})(3,4,5);
  console.log("step 2");
  await (async (...args)=>{await Promise.all(args.map(arg=>p(arg)));})(6,7,8);
  console.log("end");
})();

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/03/06 22:18

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

    キャンセル

  • 2019/03/09 23:21

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

    キャンセル

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

  • ただいまの回答率 90.45%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る

  • JavaScript

    21067questions

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