JavaScriptの非同期処理の理解と非同期処理を同期に作る三つの方法

初心者の立場でJavaScriptの非同期に対して勉強したことと非同期処理を同期に作る三つの方法について整理して見ました。
2020.04.27

今年四月に新入社員として入社したキム・スンヨンです。 私が週末の間、勉強したJavaScriptの非同期について理解したこととその非同期処理を同期に作る方法について整理して見ました。

非同期とは

非同期とは順番に進めることではなく、最初の動作の応答を受け取る前に他の作業を進めることです。

反対に同期は順番に作業を進めることです。

では、非同期に対して詳しい説明より、コードを見ましょう。

function func() {
    setTimeout(function () {
        console.log("success2");
    }, 1000);
    console.log("success1");
}

func();

結果 =>
success1
success2

このように順番に作業が進めることではなく、最初の動作の応答を受け取る前に他の作業を終えた後作業が進めます。

では、なぜ、非同期処理に今から説明する三つの方法が必要かについて説明する前に非同期が必要な理由について説明します。

非同期が必要な理由

JavaScriptの基本的な動作はSingleThreadです。

SingleThreadの動作を簡単に見るとこのような姿です。

function func() {
    console.log("start");
}

func();

結果 => start

screensh

このように動作します。

そのため、一つ作業が終わたあと次の作業を進め、色んな作業を処理する時、同期的に処理すれば最初の作業が終わるまでに他の作業を進めることができないです。

それで、非同期処理が必要です。

なぜ、非同期処理を同期に作る必要があるか

まず、コードを見ましょう。

function func(){
    for(var i=0; i<5; i++){ 
        setTimeout(function(){ 
            console.log(i); 
        },100); 
    } 
} 

func();

このコードの結果は

5 5 5 5 5 

このように出ます。

なぜ?「0,1,2,3,4」じゃないか気になりませんか?

では、なぜ、上のコードの結果が「5,5,5,5,5」かについてもっと、簡単なコードで説明します。

まず、例として、非同期処理をするためには他の「WEB APIs、Task Queue「Event Queue」、Event Loop」の助けが必要です。

function func(){     
    setTimeout(function(){
        console.log("success2");
    },0);

    console.log("success1");
}

func();

結果
success1
success2

このコード動作です。

screensh

このようにsetTimeoutの二番目のParameterを0に設定しても

success1
success2

このような結果が出ます。

では、最初のコードの動作について、理解できましたか?

順番通り、forが終了した時点からTask Queue「Event Queue」の作業がEvent LoopによってCall stackにsetTimeoutのfunctionが上がって動作したから「5,5,5,5,5」の結果を出します。

この解決するためには?それのために今から説明する三つの方法が必要です。

まず、最初説明するcallbackで解決して見ます。

function func(callback) {
    for (var i = 0; i & lt; 5; i++) { 
        callback(i); 
    }
} 

func(function (parm) { 
    setTimeout(function () { 
        console.log(parm); 
    }, 100); 
}); 

結果
0
1
2
3
4

このように解決できます。今からは非同期処理を同期に作る三つの方法について説明します。

callback

一番代表的な方法です。

一度、コードを見ましょう。

function func_start(parm_start, callback){
    var loop = setInterval(function(){
        if (parm_start === 10){
            callback(parm_start);
            clearInterval(loop);
        }else {
            parm_start++;
        }
    }, 100);
}

function func_end(parm_end) {
    console.log(`end:${parm_end}`);
}

func_start(1, func_end);

結果
end:10

callbackは全ての行動が終わった後実行されます。

コードで見た通りfunc_startの行動が終わった後、次の二番目のParameterのfunc_endが実行されます。

このcallbackを利用してAjaxの行動の次に必要な行動を同期的に作ることができます。 でも、callbackは同期的に複雑な事を作るとコードが複雑になる問題点があります。

このように重ね合わさるコードによって読みにくくなります。

簡単に見るとこのような姿です。

func1(1, function () {
    func2(2, function () {
        func3(3, function () {
            console.log('???');
        });
    });
});

この解決でPromiseがあります。

Promise

ECMAScript2015から正式に使用可能です。

まず、Promiseとは?意味は「約束」です。 JavaScriptがいつか利用する結果を生産するObjectです。

一度、コードを見ましょう。

var promise = function (param) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            if (param) {
                resolve("成功");
            } else {
                reject("失敗");
            }
        }, 1000);
    });
};

promise(false).then(function (success) {
    console.log(`Success:${success}`);
}).catch(function (err) {
    console.log(`Err:${err}`);
});

まず、このParameterから説明します。簡単にこのように考えてください。

  • resolve:解決
  • reject:失敗

そして、Promiseはいくつかの状態があります。代表的には三つです。

  • Pending「待機」:まだ、完了されていない状態
    • Promiseの生成からresolveやrejectが行動する間
  • Fulfilled「移行」:結果を送った状態
    • 状態が完了されてPromiseが結果を出した状態
  • rejected「失敗」:エラーや問題によって失敗した状態
    • 失敗してエラーなどが発生した状態

最後はThenとCatchです。

  • Then:結果をもらいます。=resolveをもらいます。
  • Catch:失敗したエラーをもらいます。=rejectをもらいます。

結果を見ましょう。

promise(true).then(function (success) {
    console.log(`Success:${success}`);
}).catch(function (err) {
    console.log(`Err:${err}`);
});

Success: 成功

promise(false).then(function (success) {
    console.log(`Success:${success}`);
}).catch(function (err) {
    console.log(`Err:${err}`);
});

Err: 失敗

このように結果が出します。 最後は非同期を処理する最近の方法です。

async/await

ECMAScript2017から正式に使用可能です。

Promiseも良い方法ですが、この方法がもっと簡単です。

まず、コード見ましょう。

async function func_start() {
    await func(2000, true);
    await func(3000, true);
    await func(1000, true);
}

function func(param_1, param_2) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            if (param_2) {
                console.log(`success:${param_1}`);
                resolve('成功');
            } else {
                reject('失敗');
            }
        }, param_1);
    });
}
func_start();

結果
success: 2000
success: 3000
success: 1000

見た通りawaitを付けたfunctionが同期的に結果を出します。

もっと簡単に同期的に作ることができます。

では、詳しい説明です。

  • async:これを付けたfunctionはいつもPromiseを出します。
    • 一度、使うとき付けるルールと覚えましょう。
  • await:これを付けるとPromiseが処理した後結果を出します。

そして、async/awaitもPromiseの理解の上で動作します。

最後に無理矢理にエラーを発生して見ました。

async function func_start() {
    await func(2000, true).then(function (success) {
        console.log(`success:${success}`);
    }).catch(function (err) {
        console.log(`Err:${err}`);
    });

    await func(3000, false).then(function (success) {
        console.log(`success:${success}`);
    }).catch(function (err) {
        console.log(`Err:${err}`);
    });

    await func(1000, true).then(function (success) {
        console.log(`success:${success}`);
    }).catch(function (err) {
        console.log(`Err:${err}`);
    });
}

function func(param_1, param_2) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            if (param_2) {
                console.log(`success:${param_1}`);
                resolve('成功');
            } else {
                reject('失敗');
            }
        }, param_1);
    });
}
func_start();

結果
success: 2000
success: 成功

Err: 失敗

success: 1000
success: 成功

このように同期的に動作します。

一度、勉強したことを整理して見ました。 最初のJavaScriptの動作についてもっと知りたい方は下の資料を見てください。

参考資料