最近 TypeScript に触れる機会が少し増えています。非同期処理についていつも思い出すのに時間がかかるので改めて覚書としてまとめます。

TypeScript は「プロを目指す人のための TypeScript 入門」 鈴木僚太著 を読んでいます。
本ページで引用元を示していない場合は上記書籍から引用しています。

非同期処理の実行

同期処理が完了した後、マイクロタスク(Promise のコールバックなど)、続いてマクロタスク(setTimeout など)がイベントループによって順に処理されます。

非同期処理の分類(マイクロタスク, マクロタスク)

JavaScript の非同期処理は、イベントループを基盤にマイクロタスクとマクロタスクに分類されます。

分類 代表的な例 実行順序の優先度
マイクロタスク Promise.then, Promise.catch, Promise.finally など コールスタック(同期処理のスタック)が空の直後に実行
マクロタスク setTimeout, setInterval, I/O など マイクロタスクがすべて完了した後に実行

実行順序の優先度の詳細(イベントループの実行順序)

  1. コールスタックが空になる
  2. マイクロタスクをすべて処理
  3. マクロタスクを 1 つ取り出して実行
  4. 1 に戻る

サンプル

1. 概要

function sampleAsynchronous() {
  return new Promise<string>(resolve => {
      console.log('First.');        // ----- (1)
      resolve('Fourth.')
  }
)};

// new Promise の中は同期的に実行されます
const promiseObj = sampleAsynchronous();

setTimeout(() => { 
    console.log('Fifth.')           // ----- (5)
},0)

console.log('Second.');             // ----- (2)


promiseObj.then((data: string) => {
  console.log(data)                 // ----- (4)
});

console.log('Third.')               // ----- (3)


// First.
// Second.
// Third.
// Fourth.
// Fifth.
実行順 タスク内容 種類 解説
(1) console.log('First.') 同期処理 sampleAsynchronous 関数内の Promise コンストラクタの中
(2) console.log('Second.') 同期処理 メインスレッドの通常の処理
(3) console.log('Third.') 同期処理 同上
(4) console.log(data) → Fourth. マイクロタスク Promise.resolve(...).then(...) のコールバック
(5) console.log('Fifth.') マクロタスク setTimeout(..., 0) によりキューに登録されたコールバック

2. 詳細

// (N) は N 番目に実行

function createPromise() {
  return new Promise<string>(resolve => {
      console.log('task 2');        // (2) -> 同期処理
      resolve('task 4')
  }
)};

function createOtherPromise() {
  return new Promise<string>(resolve => {
      console.log('task 6');        // (6) -> 同期処理
      resolve('task 8')
  }    
)}

console.log('task 1');              // (1) -> 同期処理

const promiseObj = createPromise();
promiseObj.then((data) => {        // マイクロタスクを登録
    console.log(data);             // (4) ->マイクロタスク
})

setTimeout(() => {                 // マクロタスク(1000ms後)登録 -> 後で実行
    console.log('task 5')          // (5) -> マクロタスクの同期部分 約1000ms後に実行

    const promiseOtherObj = createOtherPromise();
    promiseOtherObj.then((data) => console.log(data))

    setTimeout(() => {             // マクロタスク(この時点から2000ms後)登録 -> 後で実行
        console.log('task 9')      // (9) -> マクロタスクの同期部分 約3000ms後に実行
        console.log('task 10')     // (10) -> マクロタスクの同期部分
    }, 2000)

    console.log('task 7')          // (7) -> マクロタスクの同期部分
}, 1000)

console.log('task 3');             // (3) -> 同期処理

// task 1
// task 2
// task 3
// task 4
// task 5
// task 6
// task 7
// task 8
// task 9
// task 10
タスク 種類 実行順序 タイミング
task 1 同期処理 (1) 直ちに実行
task 2 同期処理 (2) createPromise 内で即時実行
task 3 同期処理 (3) 同上
task 4 マイクロタスク (4) 同期処理終了後、次のマクロタスク前
task 5 マクロタスク (5) 約1000ms 後
task 6 同期処理 (6) マクロタスク内の同期部分
task 7 同期処理 (7) 同上
task 8 マイクロタスク (8) task 7 の直後
task 9 マクロタスク (9) task 5 の中で登録、そこから 2000ms後
task 10 マクロタスク (10) 同上

Promise チェーン

then / catch / finally は常に新しい Promise を返し、その中で return した値が次の then に渡されます。 コールバックで何も return しない場合、次の then には undefinedd が渡されます。

function sampleAsynchronous() {
  return new Promise<number>(resolve => {
      resolve(100)
  }
)};

const promiseObj = sampleAsynchronous();

promiseObj.then((data: number) => {
  console.log(data)
  return data * 2
}).then((data: number) => {
    console.log(data)
});

// 100
// 200

async 関数

Promise をベースとして非同期処理を簡単に扱うために導入されました。

サンプル

async 関数は Promise を返します。

async function asyncFunc(): Promise<string> {
    return 'Second.';
}

const promiseObj = asyncFunc();
promiseObj.then((data: string) => console.log(data)); --- (2)

console.log('First.')                                 --- (1)

// First.
// Second.

await 式

基本

非同期処理を同期的に扱うための便利な方法として await が導入されました。

「async 関数中で await を使うことで、async 関数が返した Promise の解決を遅らせることができます。」   「逆の見方をすれば、async 関数の中で await を使えるのは、「待つ」という本来(同期的実行においては)不可能な操作を、 待ち時間を async 関数の返り値の Promise に吸収してもらうことによって可能にしているからだと言うことができます。

async function asyncFunc(): Promise<string> {
    const promiseObj = Promise.resolve('First.');
    // then コールバックはマイクロタスクとしてキューに入れられ、同期処理 `console.log('Second.')`の後に処理されます
    // そのため、Second. が先に出力され、その後に First. が出力されます
    promiseObj.then((data: string) => console.log(data) );

    console.log('Second.')

    return 'Third.';
}

const otherPromiseObj = asyncFunc();
otherPromiseObj.then((data: string) => console.log(data)); 

// Second.
// First.
// Third.

await を使用することで First -> Second. -> Third. の順で表示するように改善します。

async function asyncFunc(): Promise<string> {
    const promiseObj = Promise.resolve('First.');
-    promiseObj.then((data: string) => console.log(data) );
+    const data = await promiseObj;
+    console.log(data);
    console.log('Second.')
    return 'Third.';
}

const otherPromiseObj = asyncFunc();
otherPromiseObj.then((data: string) => console.log(data)); 

// First.
// Second.
// Third.

詳細

await は 同期処理を await が解決するまで遅らせます。

function createPromiseInAsync(): Promise<string> {
    return new Promise<string>((resolve) => {
        console.log('同期処理( in Promise Defined in Async )' );
        resolve('非同期処理( in Promise Defined in Async )');
    })
}

async function createPromiseByAsync() {
    console.log('同期処理 1( in Async )');
    const p = createPromiseInAsync();
    p.then((data) => console.log(data));
    console.log('同期処理 2( in Async )')
    return '非同期処理( in Async )';
}

async function createPromiseByAsyncWithAwait() {
    console.log('同期処理 1( in Async )');
    const data = await  createPromiseInAsync();
    console.log(data);
    console.log('同期処理 2( in Async )')
    return '非同期処理( in Async )';
}

function executeTimeout() {
    setTimeout(() => {
        console.log(`同期処理( in Timeout )`);
        Promise.resolve('非同期処理( in Promise Defined in Timeout )').then((data) => console.log(data));
        console.log('同期処理( in Timeout )')
    }, 100);
}

/*
 * 実行
 */

console.log('同期処理(トップレベル)');

executeTimeout();

// createPromiseByAsync().then((data: string) => console.log(data))

 createPromiseByAsyncWithAwait().then((data: string) => console.log(data))

createPromise().then((data: string) => console.log(data))

console.log('同期処理 (トップレベル)')

// 同期処理(トップレベル)
// 同期処理 1( in Async )
// 同期処理( in Promise Defined in Async )
// 同期処理 2( in Async )
// 同期処理( in Promise )
// 同期処理(トップレベル)
// 非同期処理( in Promise Defined in Async )
// 非同期処理( in Async )
// 非同期処理( in Promise )
// 同期処理( in Timeout )
// 同期処理(in Timeout )
// 非同期処理( in Promise Defined in Timeout )

/*
 * 同期処理 2( in Async )を非同期処理の後に遅らせる
 */

// 同期処理(トップレベル)
// 同期処理 1( in Async )
// 同期処理( in Promise Defined in Async )
// 同期処理( in Promise )
// 同期処理(トップレベル)
// 非同期処理( in Promise Defined in Async )
// 同期処理 2( in Async )
// 非同期処理( in Promise )
// 非同期処理( in Async )
// 同期処理( in Timeout )
// 同期処理(in Timeout )
// 非同期処理( in Promise Defined in Timeout )