47
67

JavaScriptの非同期処理をしっかり理解する 〜async/await/Promise〜

Posted at

JavaScript での非同期処理について、身近な例や具体例を交えながら詳細に解説しています。
最後には練習問題も用意しています!

頑張って書いているので、良いと思ったらコメント・いいね・ストック・共有などしてもらえると嬉しいです!!

非同期処理とは

非同期処理とは、プログラムの処理が順番に実行されず、ある処理を実行している間に他の処理を並行して実行することができる仕組みです。非同期処理では、あるタスクが完了するのを待たずに次のタスクが実行されるため、効率的に複数の処理を進めることが可能です。

(by ChatGPT)

非同期処理とは、「同期処理」の対義語で、同期処理は「プログラムの処理が順番に実行され、ある処理が終わるまで次の処理を待つ仕組み」です。

非同期処理の最大のメリットは、
「時間のかかる処理を行っている間に別の処理を行うことができる」
ことです!

身近な例では、部下への仕事の配分がわかりやすいです。

同期処理では、あなたがずっと仕事をしていて部下は動いておらず、時間がかかります。
1.png

非同期処理では、あなたは指示を出して部下の仕事を待って、最後に確認するだけなので早く終わります!(この上司は待っているだけなのでサボっていますが、実際には別の仕事もできます)
2.png

プログラムも同じです。
非同期処理を使って、時間のかかる処理を部下に押し付けてしまいましょう!

JavaScript における非同期処理

javaScriptでの例を示します。

例えば、以下のように1〜1000000の和を求める処理と1〜10000の積を求める処理を行いたいとします。(実際にはこのようなケースでは非同期処理は適していませんが、説明のため。)

// 1〜1000000の和を求める
let sum = 0; 
for(let i = 1; i <= 1000000; i++) {
    sum += i;
}
console.log(sum);

// 1〜10000の積を求める
let product = 1;
for(let i = 1; i <= 10000; i++){
    product *= i;
}
console.log(product);

このコードでは、1〜1000000の和を求めている間に他の処理ができません。そこで非同期処理です!

function sumAsync() {
    // メインの処理を邪魔せずに別のところで和を求める関数
}

function productAsync() {
    // メインの処理を邪魔せずに別のところで積を求める関数
}

// sumAsync() と productAsync() が両方終わるまで待って、終わったらコンソールに出力する

このように書けたらいいわけです!

ちゃんと書くとこんな感じです!(理解しなくてOKです!)

// 1〜1000000の和を求める関数
function sumAsync() {
  return new Promise((resolve) => {
    let sum = 0; 
    for(let i = 1; i <= 10000; i++) {
        sum += i;
    }
    resolve(sum);
  });
}

// 1〜10000の積を求める関数
function productAsync() {
  return new Promise((resolve) => {
    let product = 1;
    for(let i = 1; i <= 100; i++){
        product *= i;
    }
    resolve(product);
  });
}

// 和と積を別々に実装し、両方が終わるまで待ってから出力する。
Promise.all([sumAsync(), productAsync()]).then(([sum, product]) => {
  console.log("Sum:", sum);
  console.log("Product:", product);
});

Promise について

上のコードで Promise が出てきました。
これは非同期処理をしっかり理解するには重要な概念です。

まず、MDNを引用します。

プロミス (Promise) は、作成された時点では分からなくてもよい値へのプロキシーです。非同期のアクションの成功値または失敗理由にハンドラーを結びつけることができます。これにより、非同期メソッドは結果の値を返す代わりに、未来のある時点で値を提供するプロミスを返すことで、同期メソッドと同じように値を返すことができるようになります。

意味がわかりませんね。

簡単に言うと、 Promise とは「非同期で処理を行って完了したら値を返すよ」という 約束 をしているオブジェクトです。将来、値を返すので、別の言語では Future という言葉になっていることもあります。

最もシンプルに書くとこんな感じです。

const calculateSum = new Promise((resolve) => {
    const result = 1 + 1; // 実際は時間がかかる処理
    resolve(result);  // 計算できたら計算結果を返すよ〜
});

Promise はオブジェクトなので new を使って宣言します。

new Promise() の括弧内には関数が入ります。
関数の第一引数は resolve (解決)と言うコールバック関数です。
(※「コールバック関数」が何か分からなくても一旦スルーしてください)

処理が終わって Promise で返したい値が完成した時に、 resolve の引数にその値を入れて呼ぶことで、値をきちんと 解決 したものとして返すことができます。

その Promise で解決された値の受け取り方はいくつかありますが、今回は現在主流の async/await について説明します。

※後ほど、エラーが起きた時の rejectthen, catch について解説します。

async/await について

先ほどの calculateSum には何が入っているでしょうか?
実は、1+1 の計算結果ではないんです。

const calculateSum = new Promise((resolve) => {
    const result = 1 + 1; // 実際は時間がかかる処理
    resolve(result);  // 計算できたら計算結果を返すよ〜
});

console.log(calculateSum);

このように実行しても、2は返ってきません。
calculateSum1+1 の結果ではなく、 1+1 の結果を将来返すことを約束しているオブジェクトです。
なので、この場合 Promise オブジェクトがコンソールに出力されます。

これを受け取りたい時に、 await が出てきます。

const calculateSum = new Promise((resolve) => {
    const result = 1 + 1; // 実際は時間がかかる処理
    resolve(result);  // 計算できたら計算結果を返すよ〜
});

console.log(await calculateSum);

こうすることで、ようやく計算結果の 2 をコンソールに出力できます。

この await とはなんでしょうか?

await の英語的な意味は「〜を待つ」です。(wait は自動詞なので何かを待つには wait for としないといけないので他動詞の await にしているのだと思います。)

つまり、「 calculateSum の処理が完了するのを待ってその結果を受け取る」という意味になります。

どんなに早く終わる処理でも、 await を使うなどして結果を受け取りたいと明示しないと結果は受け取れず、逆に await を使えばどんなに時間がかかる処理でもそこで止まって解決を待ちます。(限界はありますがイメージです)

await の結果を変数で受け取りたい場合はこうします。

const calculateSum = new Promise((resolve) => {
    const result = 1 + 1; // 実際は時間がかかる処理
    resolve(result);  // 計算できたら計算結果を返すよ〜
});

const calculateSumResult = await calculateSum;

この時、

  • calculateSum は結果(2)を将来返すことを約束している Promise オブジェクト
  • calculateSumResult はそれが解決された結果返された値(2)
    と全くの別物であることに気をつけてください。

非同期処理を待っている間に別の処理を行う

ここまでの処理では、非同期処理を待っている間に何もしていません。これでは部下に働かせて自分は待っているだけの上司になってしまいます。

自分もちゃんと働きましょう!

const heavyJob = new Promise((resolve) => {
    const result = doHeavyJob(); // 時間がかかる重たい処理
    resolve(result);  // 計算できたら計算結果を返すよ〜
});

const anotherHeavyJobResult = doAnotherHeavyJob(); // 時間がかかる別の処理

const heavyJobResult = await heavyJob;

console.log(heavyJobResult);
console.log(anotherHeavyJobResult);

このコードは、2つの時間がかかる処理を上司(メインスレッド)と部下(Promise)で分担しています。

ここで大事なことを説明します。
Promise は、オブジェクトを作った時点で仕事(処理)が進み始めます。

例えば、doHeavyJob(), doAnotherHeavyJob() がどちらも10秒かかる処理だったとします。
1行目の const heavyJob = new Promise() の時点で doHeavyJob() の処理が非同期で進み始め、その後すぐに doAnotherHeavyJob() が同期で進み始めます。

anotherHeavyJob が終わる頃には heavyJob も終わるので、合計20秒の処理が10秒で終わります。
もし heavyJob の方が時間がかかっていたら await によって終わるまで待ち、 逆に heavyJob の方が先に終わっていたら await した瞬間に結果が返ってきます。

こうして、上司と部下で仕事を分担しつつ、両方の処理が終わってから次の処理へ進めるのです。

じゃあ、asyncとは

ここまでのコードを実行してみたら「あれ、動かないぞ?」と思った方はいませんか?
実は、 await は使える場所が限られています。

それが、 async 関数の内部 です。(厳密にはES Modulesのモジュールトップレベルでも使えます。)

async は、 asynchronous (「非同期の」という形容詞)の略で、関数につけることで「非同期の関数だよ」と宣言することになります。

関数の前に以下のようにつけることができます。

// アロー関数の場合
const asyncFunction = async () => {
    const result = await new Promise((resolve) => resolve(1 + 1));
    return result;
};

// function宣言の場合
async function asyncFunction() {
    const result = await new Promise((resolve) => resolve(1 + 1));
    return result;
}

// オブジェクトやclassのメソッドの場合
const obj = {
    async asyncMethod() {
        const result = await new Promise((resolve) => resolve(1 + 1));
        return result;
    }
};

// 即時実行関数式(IIFE)の場合
(async () => {
    const result = await new Promise((resolve) => resolve(1 + 1));
    console.log(result);
})();

async の役割は主に2つです。

  1. 関数内で await を使えるようにする
  2. 返り値を Promise でラップする(返り値が Promise になる)

つまり、上の関数の返り値は、2ではなく、 1+1 の結果を将来返す Promise になります。

const asyncFunction = async () => {
    const result = await new Promise((resolve) => resolve(1 + 1));
    return result;
};

const anotherAsyncFunction = async () => {
    const asyncFunctionPromise = asyncFunction(); // この時点ではまだ Promise
    const asyncFunctionResult = await asyncFunctionPromise; // ここで 1+1 の結果が返ってくる

    return asyncFunctionResult + 1;
    // ここで3を返すが、この関数自体も非同期関数なので、別の関数で await をするまでは Promise となる
};

このように、async のついた関数の返り値は Promise になり、別の async のついた関数でその関数の結果を使いたい時は await でその Promise の解決を待ちます。

複数の仕事が全て終わってから次に進む(Promise.all)

自分で仕事をするのではなく、全て部下にやって欲しいですよね。

でも、全部終わってから次に進みたい。
そんな時に役立つのが、 Promise.all() です。

promiseAll.js
const promise1 = new Promise((resolve) => {
    resolve(1 + 1);
});

const promise2 = new Promise((resolve) => {
    resolve(2 + 2);
});

const promise3 = new Promise((resolve) => {
    resolve(3 + 3);
});

const calculateAll = async () {
    const results = await Promise.all([promise1, promise2, promise3]); // 全てのPromiseが解決するまで待つ
    console.log(results);  // [2, 4, 6] と表示される
}

複数の Promise の配列を Promise.all() に引数で与えて、その前に await をつけることで、配列内の全ての Promise が解決するまで待ってから次の処理に移ることができます。

これで、仕事を全て部下に丸投げしても終わってから次に進めますね!

実際にどう使うか

ここまでで、 Promise async await について大体説明できました。
じゃあ、実際にどう使うのか、ここで解説していきます。

fetch

fetch.js
const fetchData = async () => {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
}

fetch は、ネットワーク等を介して、特定のURI(URL等)にHTTPアクセスを飛ばす関数です。(厳密にいうと色々ありますが)

これは非同期処理で実装されており、 Promise を返します。

ちゃんとしたWebアプリケーションを実装するとほぼ必ずAPIへのアクセスを使うことになるので、そういう時には await を使ってHTTPアクセスが終わるのを待ってからそのデータを活用しましょう。

本番ではきちんと try catch を使って例外処理を行いましょう

sleep

ゲームなどいくつかのwebアプリでは、次の処理まで何秒か待ちたい時があります。

そういう時に、以下のように書くのは良くありません。

busyWait.js
const busyWait = (ms) => {
    const start = Date.now();
    while (Date.now() - start < ms) {
        // ここでCPUを使い続ける
    }
}

const main = () => {
    doSomething();    // 何かをする
    busyWait(2000);   // 2000ミリ秒(2秒)待つ
    doAnotherthing(); // 別のことをする
}

while文に入っている間に他の処理がブロックされ、さらにCPUなども多く使ってしまいます。

そんな時は Promise を使ってこう書きましょう、

sleep.js
const sleep = async (ms) => {
    return new Promise(resolve => setTimeout(resolve, ms));
}

const main = async () => {
    doSomething();     // 何かをする
    await sleep(2000); // 2000ミリ秒(2秒)待つ
    doAnotherthing();  // 別のことをする
}

setTimeout 関数は、2つ目の引数のミリ秒後に1つ目の引数の関数を実行する関数です。
与えられた ms ミリ秒後に resolve を実行することになるので、指定したミリ秒だけ非同期で待つことができます。

setTimeout は正確にその秒数待ちたい場合には向いていません。状況に応じてブレが出ることもありますし、1ミリ秒単位の正確性はそもそもありません。

シンプルな上にメインスレッドに負担をかけない書き方ができます。

DBやファイルへのアクセス

サーバーサイドのNode.jsでの活用ですが、DBやファイルへのアクセスでも async/await は活用されることがあります。

DB(MySQL)からの読み取り

mysql.js
const mysql = require('mysql2/promise');

async function fetchFromDatabase() {
    const connection = await mysql.createConnection({
        host: 'localhost',
        user: 'root',
        database: 'test'
    });

    const [rows, fields] = await connection.execute('SELECT * FROM users');
    console.log(rows);
    connection.end();
}

ファイルの読み込み

readFile.js
const fs = require('fs').promises;

async function readFile() {
    const data = await fs.readFile('example.txt', 'utf8');
    console.log(data);
}

本番ではきちんと try catch を使って例外処理を行いましょう

then, catch, rejectについて

ここまであえて説明していない事項についての説明です。

then

実は async/await は比較的新しい記法で、以前は then を使っていました。(今でも使えます)

then.js
doSomething()
    .then(result => doSomethingElse(result))
    .then(newResult => doMore(newResult))
    .then(finalResult => console.log(finalResult));

then を使うと、 Promiseresolve された値が次の then の関数の第一引数になり、これをチェーンで繋げていけます。

具体的に書くと、

addNumbers.js
const addNumbers = (a, b) => {
    return new Promise((resolve) => {
        resolve(a + b);
    });
}

addNumbers(1, 2)
    .then(result => { // addNumbers(1, 2)の resolveの引数がresultに入る
        console.log('First result:', result);  // 1 + 2 の結果を表示
        return addNumbers(result, 3);  // 先の結果に3を足す
    })
    .then(newResult => { // addNumbers(result, 3)の resolveの引数がnewResultに入る
        console.log('Second result:', newResult);  // (1 + 2) + 3 の結果を表示
        return addNumbers(newResult, 4);  // さらに4を足す
    })
    .then(finalResult => { //  addNumbers(newResult, 4)の resolveの引数がfinalResultに入る
        console.log('Final result:', finalResult);  // (1 + 2 + 3) + 4 の結果を表示
    });

このコードの結果は以下のようになります。

First result: 3
Second result: 6
Final result: 10

同じコードを async/await で書くとこうなります。

addNumbersAsync.js
const addNumbers = (a, b) => {
    return new Promise((resolve) => {
        resolve(a + b);
    });
};

const calculate = async () => {
    const result = await addNumbers(1, 2);
    console.log('First result:', result);  // 1 + 2 の結果を表示

    const newResult = await addNumbers(result, 3);
    console.log('Second result:', newResult);  // (1 + 2) + 3 の結果を表示

    const finalResult = await addNumbers(newResult, 4);
    console.log('Final result:', finalResult);  // (1 + 2 + 3) + 4 の結果を表示
};

calculate();

この方がわかりやすいとは思いませんか?
現在では同期の表記に近いため書きやすく分かりやすい async/await が主流というわけです。

ちなみに、 then より前(Promise ができる前)はコールバック関数で全て管理していました。

callbackHell.js
doSomething(function(result) {
    doSomethingElse(result, function(newResult) {
        doMore(newResult, function(finalResult) {
            console.log(finalResult);
        });
    });
});

このように何重にもネストしたコールバック関数たちは、 コールバック地獄 と呼ばれており、読みにくく書きにくくてエラーの温床となってしまい、大きな問題でした。

catch, reject について

Promise の中でのエラーハンドリングを行うために、 rejectcatch があります。

例えば、先ほどの addNumbers で 負の数があったらエラーにしたいとします。
すると、以下のように書きます。

addNumbersReject.js
const addNumbers = (a, b) => {
    return new Promise((resolve, reject) => {
        if (a < 0 || b < 0) {
            reject('Error: Negative numbers are not allowed');
        } else {
            resolve(a + b);
        }
    });
};

addNumbers(1, 2)
    .then(result => {
        console.log('First result:', result);  // 1 + 2 の結果を表示
        return addNumbers(result, -3);  // ここで負の数を渡す。するとcatchまで処理が飛ぶ。
    })
    .then(newResult => {
        console.log('Second result:', newResult);  // (1 + 2) - 3 の結果
        return addNumbers(newResult, 4);
    })
    .then(finalResult => {
        console.log('Final result:', finalResult);  // (1 + 2 - 3) + 4 の結果
    })
    .catch(error => {
        console.error(error);  // エラーが発生した場合の処理
        // ここでは 'Error: Negative numbers are not allowed' とコンソールに表示される。
    });

Promise に渡す関数の第二引数の reject に注目です。
エラーが起きたりして値を 解決 できなかった場合には resolve() の代わりに reject() を呼び出します。
これによって、エラーが起きたことを伝えるとともに、エラーの内容を reject() の引数に入れて伝えます。

then のチェーンの中でエラーが起きて reject() が呼ばれた時、その後の then を飛ばして catch まで処理が飛びます。
この catch に与えられる関数の第一引数で reject() の引数の値を受け取り、それを元にエラー時の処理を行います。

この処理を async/await で書くとこうなります。

addNumbersRejectAsync.js
const addNumbers = (a, b) => {
    return new Promise((resolve, reject) => {
        if (a < 0 || b < 0) {
            reject('Error: Negative numbers are not allowed');
        } else {
            resolve(a + b);
        }
    });
};

const calculate = async () => {
    try {
        const result = await addNumbers(1, 2);
        console.log('First result:', result);  // 1 + 2 の結果を表示

        const newResult = await addNumbers(result, -3);  // ここで負の数を渡す。するとcatchまで処理が飛ぶ。
        console.log('Second result:', newResult);  // (1 + 2) - 3 の結果

        const finalResult = await addNumbers(newResult, 4);
        console.log('Final result:', finalResult);  // (1 + 2 - 3) + 4 の結果
    } catch (error) {
        console.error(error);  // エラーが発生した場合の処理
        // ここでは 'Error: Negative numbers are not allowed' とコンソールに表示される。
    }
};

calculate();

async/await だとこのように同期処理と同じ記法で書けるわけです。

練習問題

ここまでを振り返って問題を解いてみましょう。
解けたらぜひ、コメント欄やXで教えてください!

問題1

非同期処理 とは何か、「上司」と「部下」という言葉を使って身近な例に当てはめて説明しなさい。

問題2

以下のコードの明らかな誤りを訂正しなさい。

wrong.js
const fetchData = () => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({message: "Hello!"});
        }, 2000);  // 2秒後にデータが返ってくる
    });
};

const loadData = () => {
    const dataPromise = fetchData();
    console.log(dataPromise.message);
};

loadData();

問題3

以下のコードでコンソールのログはどの順番に並ぶか答えなさい。

console.log("Start");

setTimeout(() => {
    console.log("Timeout");
}, 1000); // 非同期で1秒待つ

const promise = new Promise((resolve) => {
    resolve(1 + 1);
});

promise.then(() => {
    console.log("Promise resolved");
});

console.log("End");

問題4

Promise を使って非同期で 1+1 を計算する処理を何も見ずに書いてみなさい。

問題5

promiseasync/await を使って指定したミリ秒の間待つsleep関数、及びそれを利用するサンプルコードを書いてみなさい。

問題6

2数の足し算を非同期で行う関数を作り、その関数を3個同時に呼び、その結果が全て出たらそれらの合計を返すコードを、 Promise.all を使って書きなさい

ちょっと宣伝

Hi!story【ハイスト】チーム(株式会社Highsto)ではメンバーを募集しています。

async/await なども活用しながらwebやモバイルのアプリ、ゲームをたくさん作っています!

もし興味があったら覗いてみてください!

Xのフォローもよろしくです!

47
67
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47
67