13 AsynchronousProcessing - iruma-tea/dokushujs GitHub Wiki

13. 非同期処理

JavaScriptでは、いたるところで非同期処理の仕組みが使われている。

13.1 非同期処理とは

非同期処理を理解するためにはスレッドについて理解する必要がある。

13.1.1 スレッド

スレッド とは、プログラムの開始から終了までの一連の処理の流れのこと。
JavaScriptのコードはJavaScriptエンジンによって1行ずつ実行されるが、その処理の開始から終了までを
一本の糸のように表すことができることを「1つのスレッドでコードが実行されている」と表現する。
ブラウザ上でJavaScriptのコードが実行されるスレッドは、メインスレッドと呼ぶ。
メインスレッドは、あくまで1つのスレッド(シングルスレッド)のため、並列してコードを実行することがない。
そのため、メインスレッド上でJavaScriptのコードが実行される場合、必ず実行中の処理の完了を待ってから次の処理が実行される
という決まりがある。また、1つの処理を複数のスレッドに分けて実行することをマルチスレッドと呼ぶ。
マルチスレッドは並列処理とも呼ばれる。

13.1.2 同期処理と非同期処理

前の処理の完了を待って次の処理を実行することを同期処理という。

  • ブラウザが提供するWeb APIの中でも、「setTimeout」、「setInterval」、「queueMicrotask」等の関数は、ブラウザの非同期処理機能を呼び出すAPIである。
  • 上記、Web APIは決められた実行手順で呼び出すことで、一部の処理(setTimeoutの場合は、引数に渡したコールバック関数)が非同期処理としてメインスレッドから一時的に切り離されることになる。

◇ 非同期処理の検証

setTimeoutを使った非同期処理の検証

let val = 0;
setTimeout(() => { val = 1;}, 1000);

console.log(val); // setTimeoutのコールバックの完了を待たず、この処理が実行される。
> 0

13.1.3 イベントループ

イベントループとは、非同期処理の管理、実行を行うための仕組みのこと

◇ イベントループ関連の用語

  • 実行コンテキスト
    • コードが実行される際にJavaScriptエンジンによって準備されるコードの実行環境のこと。
    • コードが実行される際には必ず実行コンテキストが生成される。
    • 実行コンテキストには、グローバルコンテキスト、関数コンテキスト等の種類がある。
  • コールスタック
    • 実行コンテキストが積み重なってできたものをコールスタックと呼ぶ。
    • コードが実行されるときに必ず実行コンテキストが生成されるため、コーススタックには必ずコンテキストが積まれている状態になる。そのため、コールスタックが空でない場合はメインスレッドが使用中であることを表す。
  • タスクキュー
    • 実行待ちの**タスク(非同期での実行が予約されている関数)**が格納されているキューのこと。
    • キューとはデータの出し入れをリスト形式で管理するデータ構造のことです。
    • キューからデータを取り出すときは、古いものから順番に取り出す(FIFO)。
  • イベントループ
    • タスクキューに格納されたタスクを順番に実行していく仕組み。
    • イベントループは、定期的にコールスタックを監視し、コールスタックが空のときにタスクキューから一番古いタスクを取り出して実行する。

13.1.4 イベントループの挙動

<script>
    // ① コードの実行開始
    let val = 0;

    setTimeout(function task() { // ②タスクの登録
        val = 1;
    }, 0);

    console.log(val); // ③値の出力

    > 0 // valは1ではなく0,②より前に①が実行された。

    // ④ グローバルコンテキストの消滅
</script>

13.1.5 非同期処理のハンドリング

非同期処理はコールスタックに積みあがった実行コンテキストがすべて消滅した後(以下のコードではグローバルコンテキストの消滅後)に実行される。
そのため、非同期処理で処理した値を取得して何らかの処理を行うには少し、工夫してコードを記述する必要がある。

 非同期処理内で変更した値が取得できない。
let val = -1;

function timer() {
    setTimeout(function() {
        val = Math.floor(Math.random() * 11);
    } ,1000)
}

timer();

console.log(val); // 後続処理をしても非同期処理による変更が反映されない。
> -1 

・コールバック関数を使った非同期処理のハンドリング
let val = -1;

function timer(callback) {
    setTimeout(function task() {
        val = Math.floor(Math.random() * 11);
        callback(val); // callバック関数(operations)に引数valを渡して実行
    }, 1000);
}

function operations(val) {
    console.log(val);
}

timer(operations);

> 5 // 0~10のランダムな値が1秒後にコンソールに出力される。

13.2 Promise

ES6で追加されたPromiseは、非同期処理を扱うためのオブジェクトです。
Promiseを使うことで非同期処理のネストが深くなることを避けることができるため、コードの可読性が向上する。

13.2.1 Promiseの記法

[構文] Promiseの記法
let prom = new Promise(非同期処理で扱う関数); // ①
let thenProm = prom.then(非同期処理の成功時に実行する関数); // ②
let catchProm = thenProm.catch(非同期処理の失敗時に実行する関数); // ③
let finallyProm = catchProm.finally(非同期処理完了後に必ず実行する関数); // ④

prom : Promiseのインスタンス。
thenProm : thenメソッドのコールバック関数の処理が登録されたPromiseインスタンス。
catchProm : catchメソッドのコールバック関数の処理が登録されたPromiseインスタンス。
finallyProm : finallyメソッドのコールバック関数の処理が登録されたPromiseインスタンス。

Promiseによる非同期処理のハンドリングは上記のように記述する。

  • ①の非同期を扱う関数では、引数としてresolve,rejectを使うことができる。
    • resolve()が実行された場合には②のthenコールバック関数が呼び出される。
    • reject()が実行された場合には③のコールバック関数が実行される。
    • そして④は①または③のコールバック関数が終了すると呼び出される。
 Promiseによる非同期処理

let instance = new Promise((resolve, reject) => {
    setTimeout(() => {
        const rand = Math.floor(Math.random() * 11);
        if (rand < 5) {
            reject(rand); // 5未満の場合エラーとする
        } else {
            resolve(rand); // それ以外の時は、成功とする
        }
    }, 1000);
});

instance = instance.then(value => {
    console.log(`5以上の値[${value}]が渡ってきました。`);
});

instance = instance.catch(errorValue => {
    console.log(`5未満の値[${errorValue}]が渡ってきたためエラー表示`);
});

instance = instance.finally(() => {
    console.log("処理を終了します。");
}); 

13.2.2 Promiseチェーン

複数のPromiseを直列で実行する。直列とは、前の非同期処理の完了を待って、次の非同期処理を実行することを意味する。
複数のPromiseによって非同期処理を順番に実行していくことwPromiseチェーンと呼ぶ。

[構文] Promiseチェーンの記述方法
const Promiseインスタンス1 = new Promise(...);
const Promiseインスタンス2 = new Promise(...);
const Promiseインスタンス3 = new Promise(...);

Promiseインスタンス1
    .then(data1 => {return Promiseインスタンス2;})
    .then(data2 => {return Promiseインスタンス3;})
    .catch(error => {エラー発生の処理})
    .finally(() => {終了の処理});
 Promiseチェーンの記述例

function promiseFactory(count) {
    return new Promise((resolve,reject) => {
        setTimeout(() => {
            count++;
            console.log(`${count}回目のコールです。時刻:[${new Date().toTimeString()}]`);
            if (count === 3) {
                reject(count);
            } else {
                resolve(count);
            }
        }, 1000);
    });
}

promiseFactory(0)
    .then(count => {return promiseFactory(count);})
    .then(count => {return promiseFactory(count);})
    .then(count => {return promiseFactory(count);})
    .then(count => {return promiseFactory(count);})
    .catch(errorCount => {
        console.error(`エラーに飛びました。現在のカウントは${errorCount}です。`)
    }).finally(() => {
        console.log("処理を終了します。");
    });

> 1回目のコールです。時刻:[13:04:25 GMT+0900 (日本標準時)]
> 2回目のコールです。時刻:[13:04:26 GMT+0900 (日本標準時)]
> 3回目のコールです。時刻:[13:04:27 GMT+0900 (日本標準時)]
> エラーに飛びました。現在のカウントは3です。
> 処理を終了します

13.2.3 Promiseの状態管理

Promiseのインスタンスは、内部で現在の状態(ステート)を管理し、これによって、
Promiseのメソッド(catchやthen)などの呼び出しを制御している。

ステータス 説明
pending resolve,rejectが呼び出される前の状態
fulfilled resolveが呼び出された状態
rejected rejectが呼び出された状態

Promiseのステータスの状態は、Promiseインスタンスをコンソールに出力することで確認できる。

 Promiseインスタンスのステータスを確認

let promResolve, promReject;

const prom = new Promise((resolve, reject) => {
    promResolve = resolve;
    promReject = reject;
});

console.log(prom);
> Promise {<pending>}

promResolve("引数");
// promReject();

console.log(prom);
> Promise {<fulfilled>: "引数"}

13.2.4 Promiseを使った並列処理

Promiseの静的メソッドを使って、非同期処理を並列で行う。

◇ Promise.all

すべての非同期処理を並列に実行し、すべての完了を待ってから次の処理を行いたい場合に、Promise.allを使用する。

[構文] Promise.allの記法
Promise.all(iterablePromises)
    .then((resolvedArray) => {....})
    .catch((error) => {...})

iterablePromises: Promiseインスタンスを含む反復可能オブジェクト(ArrayやSet)を設定する。     
resolvedArray: iterablePromisesに格納された各Promiseのresolveの実引数が格納された配列となって渡されてきます。
この配列の順番はiterablePromisesに格納されているPromiseの順番に一致します。
error: 最初にrejectedになったインスタンスのrejectの引数の値が渡ってくる。
戻り値: Promiseのインスタンスが返される。
 Promise.allの記述例

function wait(ms) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`${ms}msの処理が完了しました。`);
            resolve(ms);
        }, ms);
    });
}

const wait400 = wait(400);
const wait500 = wait(500);
const wait600 = wait(600);

Promise.all([wait500, wait600, wait400])
    .then(([resolved500, resolved600, resolved400]) => {
        console.log("すべてのPromiseが完了しました。");
        console.log(resolved500, resolved600, resolved400)
    });

◇ Promise.race

Promise.raceは、複数のPromiseインスタンスの何れかの状態が、settled(fulfilledまたはrejected)になったとき、
Promise.raceに続くthenメソッドまたはcatchメソッドを実行する

[構文] Promise.raceの記法
Promise.race(iterablePromises)
    .then((firstResolvedValue) => {...})
    .catch((error) => {...})

iterablePromises: Promiseインスタンスを含む反復可能なオブジェクト(ArrayやSet)を設定する。
firstResolvedValue: 最初にfulfilledになってインスタンスのresolveの引数の値が渡される。
error: 最初にrejectedになったインスタンスのrejectの引数の値が渡される。
戻り値: Promise.allSettledのインスタンスが返却される。
 Promise.raceの記述例
const myResolve = new Promise(resolve => {
    setTimeout(() => {
        resolve("resolveが呼ばれました。");
        console.log("myResolveの実行が終了しました。");
    }, 100);
});

const myReject = new Promise((_, reject) => {
    setTimeout(() => {
        reject("rejectが呼ばれました。");
        console.log("myRejectの実行が終了しました。");
    }, 200)
});

Promise.race([myResolve, myReject])
    .then(value => {
        console.log(value);
    }).catch(value => {
        console.log(value);
    });

> myResolveの実行が終了しました。
> resolveが呼ばれました。
> myRejectの実行が終了しました。

◇ Promise.any

Promise.anyは、複数のPromiseインスタンスの何れかがfulfilledになったタイミングでthenメソッドに処理を移す。
また、すべてのインスタンスの状態がrejectedになったときに、catchメソッドを実行する

[構文] Promise.anyの記法
Promise.any(iterablePromises)
    .then((resolvedValue) => {...})
    .catch((error) => {...});

iterablePromises: Promiseインスタンスを含む反復可能オブジェクト(ArrayやSet)を設定する。
resolvedValue: 最初にfulfilledになったインスタンスのresolveの引数の値が渡されてきます。
error: AggregateErrorという特殊なオブジェクトです。
戻り値: Promiseのインスタンスが返される。
Promise.anyの記述例

const myResolve = new Promise(resolve => {
    setTimeout(() => {
        resolve("resolveが呼ばれました。");
        console.log("myResolveの実行が終了しました。");

    }, 200);
});

const myReject = new Promise((_, reject) => {
    setTimeout(() => {
        reject("rejectが呼ばれました。");
        console.log("myRejectの実行が終了しました。");
    }, 100);
});

Promise.any([myReject, myResolve])
    .then(value => {
        console.log(value);
    }).catch(error => {
        console.log(error);
    });

myRejectの実行が終了しました。
myResolveの実行が終了しました。
resolveが呼ばれました。

◇ Promise.allSettled

Promise.allSettledは、すべてのPromiseインスタンスの状態がsettled(fulfilledまたは、rejected)になったときに、
thenメソッドに処理を移行する。

[構文] Promise.allSettled
Promise.allSettled(iterablePromises).then((arry) => {...})

iterablePromises: Promiseインスタンスを含む反復可能オブジェクト(ArrayやSet)を設定します。
arry: Promiseインスタンスの状態と値が対になって格納された配列が渡される。
    [
        {status: "fulfilled", value: "resolveの値"},
        {status: "rejected", value: "rejectの値"}
    ]
戻り値: Promiseのインスタンスが返される。
 Promise.allSettledの記述例

const myResolve = new Promise(resolve => {
    setTimeout(() => {
        resolve("resolveが呼ばれました。");
        console.log("myResolveの実行が終了しました。");
    }, 200);
});

const myReject = new Promise((_, reject) => {
    setTimeout(() => {
        reject("rejectが呼ばれました。");
        console.log("myRejectの実行が終了しました。");
    }, 100);
});

Promise.allSettled([myReject, myResolve])
    .then(arry => {
        for(const {status, value, reason} of arry) {
            console.log(`ステータス:[${status}],値:[${value}], エラー:[${reason}]`);
        }
    });

> myRejectの実行が終了しました。
> myResolveの実行が終了しました。
> ステータス:[rejected],:[undefined], エラー:[rejectが呼ばれました。]
> ステータス:[fulfilled],:[resolveが呼ばれました。], エラー:[undefined]

13.2.5 その他の静的メソッド

◇ Promise.resolve(引数)

Promise.resolve は、fulfilledの状態のPromiseインスタンスを返す。
特定の処理を非同期処理として実行したい場合に使う。

 Promise.resolveの使用例

let val = 0;

Promise.resolve().then(() => {
    console.log(`valの値は[${val}]です。`);
});

val = 1;
console.log("グローバルコンテキストの終了");

> ローバルコンテキストの終了
> valの値は[1]です。

◇ Promise.reject(引数)

Promise.reject は、rejected状態のPromiseインスタンスを返す。
基本的にはPromise.resolveと同じであるが、エラーのみ非同期処理とする実装はほとんどないため、
参考程度に知っておくで良い。

 Promise.rejectedの使用例

Promise.reject("エラーの理由").catch(error => {
    console.log(error);
});

console.log("グローバルコンテキストの終了");

> グローバルコンテキストの終了
> エラーの理由
> undefined

13.3 await/async

ES2017のバージョンからawait/asyncキーワードを使用して、Promiseのthenより簡潔に記述できるようになった。
これにより、Promiseチェーンの記述を簡略化できる。

13.3.1 async

async は、関数の先頭につけると、**非同期関数(AsyncFunction)**という特殊な関数を定義することができる。
記法は以下の通り。

[構文] async
- 非同期関数の宣言
async function 関数名() {...};

- 無名関数やアロー関数の先頭にも付与できる
someFunction(async() => {...});

- オブジェクトやクラスのメソッドにも付与できる。
class MyClass {
    async method() {...}
}
const obj = { method: async function() {...}};

非同期関数のreturnが返す値は必ずPromiseインスタンスになる。

 asyncはPromiseを必ずかえす。
async function asyncFunction() {
    return "hello";
}

asyncFunction().then((returnValue) => {console.log(returnValue)});

> hello

13.3.2 await

awaitは、Promiseインスタンスの前に記述することで、Promiseのステータスがsettled(fulfilledまたはrejected)になるまで、後続のコードの実行を待機するなお、awaitは非同期関数内(async function)でしか使用できない。

[構文] awaitの記法
async function 関数名() {
    let resolvedValue = await prom;
}

prom: Promiseインスタンス。
resolvedValue: Promiseインスタンス内でのresolveの実引数の値がawaitの結果として返される。

awaitは、Promise内のresolveの実引数の値を取り出す役割もある。
 awaitはresolveの実引数の値を取り出す。

const prom = new Promise(resolve => {
    setTimeout(() => resolve("この値を取り出す。"), 1000);
});

async function asyncFunction() {
    const value = await prom;
    console.log(value);
}

asyncFunction();
> この値を取り出す。

 Promiseがrejectedになった場合
async function throwError() {
    try {
        await Promise.reject("Promiseが失敗しました。");
    } catch (error) {
        console.log(error);
    }
}

throwError();
> Promiseが失敗しました。
 await/asyncを使ったPromiseチェーンの置き換え
function promiseFactory(count) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            count++;
            console.log(`${count}回目のコールです。時刻:[${new Date().toTimeString()}]`);

            if (count === 3) {
                reject(count);
            } else {
                resolve(count);
            }
        }, 1000);
    });
}

async function execute() {
    try {
        let count = await promiseFactory(0);
        count = await promiseFactory(count);
        count = await promiseFactory(count);
        count = await promiseFactory(count);
    } catch (errorCount) {
        console.error(`エラーに飛びました。現在のカウントは${errorCount}です。`);
    } finally {
        console.log("処理を終了します。");
    }
}

execute();

> 1回目のコールです。時刻:[19:17:35 GMT+0900 (日本標準時)]
> 2回目のコールです。時刻:[19:17:36 GMT+0900 (日本標準時)]
> 3回目のコールです。時刻:[19:17:37 GMT+0900 (日本標準時)]
> エラーに飛びました。現在のカウントは3です。
> 処理を終了します。

13.4 Fetch

サーバーからデータやファイルを取得するときに使う、Web APIの一種。
fetch関数は非同期処理となるため、取得したデータを使って処理する場合、Promiseやawait/asyncを使ってコードを記述する。

[構文] fetchの記法
fetch("リクエストURL"[, data])
    .then(response => response.json())
    .then(data => {取得したJSONを使って処理を行うコード});

リクエストURL: リクエストを送信する先のURLを文字列で渡す。
戻り値: fetch関数を実行すると、response(Responseオブジェクト)がPromiseにラップされた値で返される。
data: リクエスト送信時の設定をオブジェクトにして渡す。代表的なプロパティは以下の通り。
    |プロパティ|説明|
    |method|POST | GET | PUT | DELETE |などのリクエストメソッドを文字列で指定する。初期値はGET|
    |headers|リクエストヘッダーを変更するときに設定する。(オブジェクト形式)|
    |body|リクエストのbody部を挿入したい値を設定する|

response: サーバから返された情報を保持するResponseオブジェクト。
    |プロパティ|説明|
    |Response.ok|200~299のHTTPステータスがサーバーから返された場合は、リクエスト成功としてtrueが格納されている。それ以外はリクエストの失敗としてfalseが返る。|
    |Response.status|HTTPステータスが格納されている|
    |Response.headers|レスポンスヘッダーの情報がオブジェクトで格納されている。|
    |メソッド|説明|
    |Response.json()|レスポンスで返ってきたJSON文字列を処理する時に使う。|
    |Response.blob()|バイナリーデータ(0と1の羅列)を含むレスポンスを処理するときに使う。|
    |Response.text()|レスポンスの文字列を取得するときに使う。|
 fetchを使ったJSONの取得
(取得データ(sample.json))
[
    {"key": "apple", "value": "リンゴ"},
    {"key": "orange", "value": "オレンジ"},
    {"key": "melon", "value": "メロン"}
]

<script>
    fetch("sample.json");
        .then(response => response.json())
        .then(data => {
            for (const {key, value} of data) {
                console.log(key + ":" + value);
            }
        });
    
    /* await/asyncを使用した場合 */

    async function myFetch() {
        const response = await fetch("sample.json");
        const data = await response.json();
        for (const {key, value} of data) {
            console.log(key + ":" + value);
        }
    }
    myFetch();
</script>
⚠️ **GitHub.com Fallback** ⚠️