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

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

ただいまの
回答率

88.77%

【初心者】Node.jsの同期処理が全くわからなくて困っています...

解決済

回答 1

投稿

  • 評価
  • クリップ 1
  • VIEW 2,465

vac

score 11

状況説明

私は現在、簡単なTODOアプリケーションをNode.jsで作成しようとしているのですが、同期処理の実装に困ってしまい、長い時間解決に至らなかったために相談させていただきました。

今実装したいこととしては、タスクのIDをあるURLにPOSTすると、そのタスク情報をMongoDBから取得してJSON形式で返すというAPIを実装したいと思っています。ですが、Node.jsのモジュール群は非同期の関数で実装されているとのことで、思った挙動が得られない状態です。

同期処理についていくらか調べてみて、Promiseという概念が用いられることとasync/awaitで実装されるとよいということまでは理解したのですが、それを踏まえて自分なりに実装してみてもうまく動かないようでした。動かない要因等をお教えいただけると非常に嬉しいです。

問題のコード

以下がルーティングの部分です。/task/getoneにtaskId付きでPOSTすることを想定しています。この状態だと、retが空のままres.json(ret)が実行されて、空のbody.dataでレスポンスが返ってきます。
自分なりに考えてasync/awaitを追加してみたのですが、思うように動きません。

var GetOneTask = require('../controller/task/getOneTask.js');

---(中略)---

router.post('/task/getone', async function (req, res, next) {
        console.log(req.body);
        var body = req.body;
        var ret;
        if (body.taskId==null){
            ret = {
                state: 'failure',
                description: 'Task Id is empty.'
            };
        } else {
            ret = await GetOneTask(body.taskId);
        }
        res.json(ret);
    })


以下はDB操作をする関数です。上のコードでrequireされます。
async関数にしてみていますが、思うような挙動は得られませんでした。

module.exports = async function (taskId) {
    // モデルの宣言
    var Task = require('../../model/Task');

    Task.findById(taskId, function(err, result){
        if (err) {
            console.log(err)
            return {
                state: 'failure',
                description: 'Find Error'
            }
        } else return {
            state: 'success',
            data: result
        }
    })
}

実行結果

body.dataにデータを付与してレスポンスとして返したいのですが、空っぽのまま200番が返ってしまいます。

聞きたいこと

  • これではなぜ動かないか
  • どのようにすれば期待通りの同期処理が得られるか
  • 気になる質問をクリップする

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

回答 1

checkベストアンサー

+3

getOneTaskの作りに問題があって動きません。
非同期 → Promise → async/awaitの移行は実は完璧ではなく、
開発者へ大きな負担を強いる原因となっています。

Promise → async/awaitはどういうコードなのかを記述します。
asyncやawaitは厳密には糖衣構文で、ES6のコードを簡略にしただけの機能です。

  • async: 宣言した箇所で関数内全体をnew Promise()で包みます。
    その後、return宣言した箇所の戻り値の部分resolve()で包んで状態を完了とし、
    throwでエラーを投げようとしたら、reject()で包んで状態を失敗に変更します。
  • await: 右辺のPromiseの状態が完了になるまで一生待ち、完了になったら.thenメソッドを実行して値を取り出します。

つまり、質問文のコードをES6相当に書き直すとこうなります。

module.exports = (taskId) => new Promise((resolve, reject) => {
  var Task = require('../../model/Task');
  Task.findById(taskId, function(err, result){
    if (err) {
      console.log(err)
      return {
        state: 'failure',
        description: 'Find Error'
      }
    } else return {
      state: 'success',
      data: result
    }
  })
})

これは明らかに駄目なコード。
Promiseの状態が保留から完了・失敗に変化する為には
new Promiseを実行する際に渡したコールバック関数の第一引数や第二引数を実行する必要があります。
Promise - MDN

次に正解を書きましょう。
getOneTaskが正常になったから確実に動くとは限りませんが、前には進めるはずです。

// requireはファイルの冒頭で記述するべき
var Task = require('../../model/Task');

// 従来の非同期関数を使う場合、
// 1行目でPromiseを返す作りにする必要がある
module.exports = taskId => new Promise((resolve, reject) => {
  Task.findById(taskId, (err, result) => {
    if (err) {
      console.log(err)
      // ホントはrejectの方がふさわしいと思うけどね。
      resolve({
        state: 'failure',
        description: 'Find Error'
      })
    } else {
      resolve({
        state: 'success',
        data: result
      })
    }
  })
})

コールバック関数の中身でresolve({state: 'success', data: result})等という風にresolveを実行しましたね。
これでPromiseの状態が保留から完了に変化し、次へ進めるようになります。

次に何故getOneTask内でasync関数を作ってはならないのかを見に行きましょう。
一見これでも行けそうじゃんというasync関数のコードとそのES6版のコードを同時に書きました。

const main = async (taskId) => {
  return await Task.findById(taskId, (err, result) => {
    if (err) throw err
    return result
  })
}

// 下記のコードと等価
const main = taskId => new Promise((resolve, reject) => {
  resolve(Task.findBy(taskId, (err, result) => {
    if (err) throw err
    return result
  }))
})

Task.findByは昔ながらの非同期処理です。
returnする際は一度task.findByのコールバック関数の中身に潜ってから
resolve(result)みたいな事をして欲しいですよね?

おいおい、お前全然関係無いところでresolveしとるやないか!!となるわけです。
これじゃ駄目、なので全てPromise → Promiseで繋いで行くならasync/awaitをフル活用してスマートに書けるのですが、大昔の非同期処理の部分は一度promiseを返してよしなにやってくれる関数で包まなければなりません。

投稿

編集

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2019/01/16 22:02

    ここまで詳しい回答がいただけるとは思っていませんでした...!とても感謝しています!
    提示していただいたコードを参考にして、無事動かすことができました。ありがとうございます。

    async/awaitは糖衣構文なのですね。非常に勉強になりました。自分もそれくらいの粒度でNode.jsのコードが読み解けるようになりたいものです...

    キャンセル

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

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

関連した質問

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