WebEngine

だらだらと綴る技術系メモ

非道な非同期処理を理解したい[JavaScript]

目次

非同期処理って理解しづらいですよね。でも、分解して見ていけば、きっとわかるはず!

  1. 同期処理
  2. 非同期処理
  3. コールバック
  4. Promise
  5. async/await
  6. 参考

※ 注意
Google Chormeでしかテストしていません。
各項目での説明やソースコードは参考にした記事をちょっと変えただけ、というものが多いです。それらのリンクは最後の「参考」に掲載してあります。

1. 同期処理

console.log(1);
console.log(2);
console.log(3);

こんな処理があったとします。結果は順に 1、2、3 と表示される、一般的な 処理です。JavaScriptは、関数をキューに登録し、順番に1つずつ実行 していくみたいです。この処理では、log(1)log(2)log(3)と 順番に処理していくので結果も、それに倣って出力されるわけです。

JavaScriptでは、上から順にキューに登録していく処理を同期処理と呼ぶ ...みたいです。

2. 非同期処理

console.log(1);
setTimeout(() => { console.log(2); }, 1000);
console.log(3);

処理を遅らせるsetTimeoutを使っています。この場合、log(1)setTimeout()log(3)の順にキューに登録されます。

キューに登録されたとおり、まずlog(1)が実行され、1が出力されます。次に setTimeoutが実行され、タイマーにlog(2)が登録されます。つづいて log(3)が実行され、3が表示されます。ここまでは同期処理になります。

タイマーに設定されていたlog(2)は1000ミリ秒後にキューに登録されます。log(2)は、 最初の3つの関数とは別物としてキューに登録されるので、非同期処理と呼ばれます。

結果、最後にlog(2)が実行されて、1、3、2 と出力されるわけです。


この非同期で、独立してキューに登録されるという事実を知っておくと以下のような処理も理解できます。

console.log(1);
setTimeout(() => { console.log(2); }, 0);
console.log(3);

0ミリ秒後にセットされているので、非同期処理を知らない人は 1、2、3 と出力されると考えがちです。
しかし実際には、setTimeoutが実行されたあと、log(2)が独立してキューに登録されるので、0ミリ秒後となっていても 1、3、2 と出力されます。


非同期処理は、外部APIへのアクセスやDBまわりの処理等で使います。ざっくりいうとバックグラウンドでの処理に近いですね。

3. コールバック

JavaScriptでは関数の引数に関数を渡すことが可能です。
この機能を利用して、ある処理Aが無事終了した場合、引数に設定していた関数の処理Bを実行するといった処理の流れを 実装できます。非同期処理は、そうしなければならない処理なので必然的にコールバックが出てくるわけです。

コールバックは無名関数でなくとも良いです。

下記処理は上と同じ非同期処理で、結果も同じです。

const logFunc = function() {
    console.log(2);
}
console.log(1);
setTimeout(logFunc, 0);  // 先程は第1引数が無名関数だった
console.log(3);


無論、このsetTimeoutの部分が自作の関数でも良いです。

// 渡される引数の関数は何でも良い
function mainFunc(callback) {
    callback();  // 引数で渡した関数を実行
}
// コールバック関数
const argsFunc = function() {
    console.log("Hello, Callback!");
}
mainFunc(argsFunc);


4. Promise

非同期処理を書く際には、コールバックを記述する必要が出てくることを説明しましたが、コールバックを書く際に陥りがち なのがコールバック地獄です。そのコールバック地獄を回避するために生まれたのがPromiseです。

// Promiseオブジェクトを返す
function asyncFunc() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Hello!');
        }, 1000);
    });
}
// -- Promiseフロー --
asyncFunc().then(value => {
    console.log(value);  // 成功時処理
    console.log('asyncFunc success!');  // asyncFuncが終了するまでは実行されない
}).catch(error => {
    console.log(error);  // エラー時処理
});
// -- Promiseフロー --
console.log('asyncFunc run?');  // asyncFuncの実行が終わってなくても出力される

一定時間が経過するとHello!という文字列を返すasyncFunc関数があるとします。中では、Promiseオブジェクトを 返すように実装してあります。結果は次のとおり。

asyncFunc run?
Hello!
asyncFunc success!

非同期処理なので、まず、処理順が確定されていない最後のログasyncFunc run?が出力されます。このときasyncFunc success!は 出力されていませんよね? ということは、Promiseオブジェクトを返し、その返した関数をどう扱うか記述したフロー(ここではコメントアウト「Promiseフロー」 でくくった箇所を指す)では、Promiseオブジェクトがリターンされるまで処理が実行されないことが確約されます。

要はPromiseの登場で、安心して非同期処理が書けるようになったのでした。


Promiseは、jQueryのメソッドチェーンみたいに連続した非同期処理を記述することも可能です。

// 1秒後に引数の値を返す
function asyncDelay(num) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(num);
        }, 1000);
    });
}
// 計算した値を返す
function asyncCalc() {
    let result = 0;
    return asyncDelay(5)
        .then(value => {
            console.log(value);
            result += value;
            return asyncDelay(10);
        })
        .then(value => {
            console.log(value);
            result *= value;
            return asyncDelay(20);
        })
        .then(value => {
            console.log(value);
            result += value;
            return result;
        });
}
// -- main ---
asyncCalc().then(value => {
    console.log(value);
    console.log("計算終了");
});
console.log('同期処理');
同期処理
5
10
20
70
計算終了


5. async/await

先程、連続した非同期処理をPromiseで記述しましたが、もうちょっとスタイリッシュに書けるんじゃないか、と思った方もいるのではないでしょうか。 そんなわけでできたのがasync/awaitです。

Promiseの項目で書いた連続する非同期処理をasync/awaitを使って実装したものがこちら。

// 1秒後に引数の値を返す
function asyncDelay(num) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(num);  // ログで確認するため追加
            resolve(num);
        }, 1000);
    });
}
// 計算した値を返す
async function asyncCalc() {
    return await asyncDelay(5) * await asyncDelay(10) + await asyncDelay(20);
}
// -- main ---
asyncCalc().then(value => {
    console.log(value);
    console.log("計算終了");
});
console.log('同期処理');

結果はPromiseを使って書いたソースコードと同じになるはずです。
asyncCalc内は1行で済ませていますが、次のように書くことも可能です。

// 計算した値を返す
async function asyncCalc() {
    const a = await asyncDelay(5);
    const b = await asyncDelay(10);
    const c = await asyncDelay(20);
    return a * b + c;
}


asyncは定義する関数の前につけます。こいつを装着した関数はコールされるとPromiseを返すようになります。
上記コードを見てもわかるとおり、Promiseを返しているのでthenなんかが使えています。

awaitを前につけると、つけた関数の値が返されるまでasync関数内の処理を中断します。結果が返ってくると、 処理は再開されます。awaitasync関数内でしか指定できません。

ここで注意が必要なのは、awaitで指定した関数で返ってくるのはPromiseではなく値だということです。

// 1秒後に引数の値を返す
function asyncFunc(num) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(num);
        }, 1000);
    });
}
// awaitなし
async function checkAsyncFunc() {
    const a = asyncFunc(1);
    console.log(a);
    return a;
}
// -- main --
checkAsyncFunc().then(value => {
});

結果はPromiseがリターンされてきます。

Promise {<pending>}

awaitをつけるとどうなるか。

// awaitあり
async function checkAsyncFunc() {
    const a = await asyncFunc(1);
    console.log(a);
    return a;
}

今度はPromiseではなく、値が返ってきていることがわかります。

1

当たり前じゃん、と思う方もいると考えますが、ここら辺が曖昧だと、返ってきた値に対してthenなんかをつけて、エラーが 起きる、みたいなことがあります。


さて、ざっと非同期処理について見てきましたが、今はasync/awaitを使って非同期処理を書くのが主流になってきているようです。
Promiseasync/awaitに至るまでの過渡的なものだった、という見方ができますが、当然async/awaitよりもスマートな書き方が 出てきてもおかしくありません。そういうときは、「また勉強しなきゃいけないか」という気持ちではなく、「また便利なのが出てきた、やったー」なんて 気持ちを持つことが大事だと思います。

6. 参考