非同期処理(JavaScript)、コールバック、Promise、async/await

非同期処理(JavaScript)

以下についてそれぞれ調べた。

  • 非同期処理
  • callback
  • promise
  • async/await

同期処理と非同期処理について

同期処理

複数のタスクを実行する際に一つずつ順番にタスクが実行される方式のこと。 プログラムに記載した通りの順番でタスクが処理される。

非同期処理

あるタスクを実行している最中に、その処理を止めることなく別のタスクを実行できる方式のこと。 コードの順番を無視してタスクが処理される。

メリット:うまく活用することで、アプリケーション全体の処理速度を早められる。 デメリット:プログラムの全体像が複雑になりやすい

JavaScriptで非同期処理を実装するためには、callback、Promise、Async/Awaitなどが必要。

非同期処理とは? 同期処理との違い、実装方法について解説

JavaScriptのコールバックについて

コールバック(関数)とは

一言で表すと、「引数として渡される関数」

    setTimeout(function() {
        console.log('Hello');
    }, 2000);

とあった場合、setTimeout()の中に入っている関数がコールバック。

コールバック(関数)と非同期処理

「特定の非同期処理の後のタイミングで何かを実行したい」時にコールバック関数を使う。

何かしらの非同期処理関数の中にコールバック関数を入れておいて、その非同期処理関数の処理が終わったらコールバック関数を呼び出す

JavaScriptの非同期処理の実現方法の一つ。

例) XHRを利用する時にコールバックを使う XHRは外部ファイルを読み込むことができるが、基本的に非同期処理。 しかし、「ファイルを読み込み終わった後にxxxする」という処理をしたい場合、非同期処理ではそのままコードを書いただけではうまくいかない。

うまくいかないコード例

const xhr = new XMLHttpRequest();
xhr.open('GET', 'foo.txt');
xhr.send();
console.log(xhr.responseText);

このコードだと、ファイルの読み込みが終わる前にconsole.logが実行されてしまう。

そこでコールバックを使い、こう書き換える。

const xhr = new XMLHttpRequest();
xhr.open('GET', 'foo.txt');
xhr.addEventListener('load', (event) => console.log(xhr.responseText));
xhr.send();

addEventListenerはイベントに対応するコールバック関数を登録するメソッド。 XHRのイベントloadが発生した後にconsole.logが実行されるように指定している。

Promiseとasync/awaitでJavaScriptの非同期処理をシンプルに記述する

コールバック地獄

コールバックだけで非同期処理を制御しようとすると、コードがどんどん複雑化する。

例) 三つのファイルを順番に読み込むコード

const xhr = new XMLHttpRequest();
xhr.open('GET', 'foo.txt');
xhr.addEventListener('load', (event) => {
    const xhr2 = new XMLHttpRequest();
    xhr2.open('GET', 'bar.txt');
    xhr2.addEventListener('load', (event) => {
        const xhr3 = new XMLHttpRequest();
        xhr3.open('GET', 'baz.txt');
        xhr3.addEventListener('load', (event) => console.log('done!'));
        xhr3.send();
    });
    xhr2.send();
});
xhr.send();

コードのネストが深くなるせいでコールバックの入れ子まみれになる。 これが「コールバック地獄」

コールバック地獄はコードの品質を低下させ、さまざまな問題を発生させる。

解決法として、Promiseを使う方法がある。

Promiseについて

Promiseは、非同期処理を簡単に扱うための仕組み。

PromiseをnewすることによりPromiseオブジェクトを作成(Promiseのコンストラクタには、実行したい処理を書いた関数を渡す)。 処理が済んだらresolve関数を呼び出すことで終了を明示。 Promiseオブジェクトのthenメソッドに、Promise終了後に処理したい関数を渡す。

これらによって、「Promiseの実行が済んだ後にxxxする」という処理を書くことができる。

例) 500ミリ秒後に「hello」を表示した後に「world」を表示する。

const promise = new Promise((resolve, reject) => {
    setTimeout(() => { 
        console.log('hello');
        resolve();
    }, 500);
});

promise.then(() => console.log('world!'));

resolveされた後にthenが呼ばれる仕組みになっている。 このようにして非同期処理の実行順序を記述できる。

thenチェーン

thenをメソッドチェーンで繋げて書くことができる。 thenメソッドをチェーンすることで、複数の非同期処理を直列に書くことができる。

例) 一定時間ごとに特定の文字列を表示するコード

function printAsync(text, delay) {
    const p = new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(text);
            resolve();
        }, delay)
    });
    
    return p;
}

printAsync('hello', 500)
    .then(() => printAsync('world', 500))
    .then(() => printAsync('lorem', 500))
    .then(() => printAsync('ipsum', 500));

Promiseを使うことで、コールバック地獄になっていたコードを簡潔にすることができる。

function openFile(url) {
    const p = new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', url);
        xhr.addEventListener('load', (e) => resolve(xhr));
        xhr.send();
    });
    
    return p;
}

openFile('foo.txt')
    .then((xhr) => openFile('bar.txt'))
    .then((xhr) => openFile('baz.txt'))
    .then((xhr) => console.log('done!'));

入れ子構造が消え、thenによるチェーンになることで非常に読みやすくなる。

Promise.all

thenをチェーンして直列に書くほか、Promise.allを使って複数のPromiseの終了を待つこともできる。

function openFile(url) {
    const p = new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', url);
        xhr.addEventListener('load', (e) => resolve(xhr));
        xhr.send();
    });
    
    return p;
}

const promise = Promise.all([openFile('foo.txt'),
                             openFile('bar.txt'),
                             openFile('baz.txt')]);
promise.then((xhrArray) => console.log('done!'))

async/awaitについて

Promiseを使ってさらに非同期処理を簡潔に書くための仕組みがasync/await

asyncがついた関数内でawaitをPromiseにつけると、そのPromiseの処理が終了するまで、コードの実行が先に進まなくなる。

awaitを利用することで、非同期処理をまるで同期処理のように書くことができる。

function openFile(url) {
    const p = new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', url);
        xhr.addEventListener('load', (e) => resolve(xhr));
        xhr.send();
    });
    
    return p;
}

async function loadAllFiles() {
    const xhr1 = await openFile('foo.txt');
    const xhr2 = await openFile('bar.txt');
    const xhr3 = await openFile('baz.txt');
    console.log('done!');
}

loadAllFiles();