はじめに
この記事では他の言語を触ったことあるけどJavaScriptは初めて触った程度の自分がJSの非同期処理をなんとなく理解するまでに学習したことをまとめています。自分も勉強中なのでもし誤りがあればご容赦ください。
目次
- JavaScriptとは
- 基本構文(変数、関数、オブジェクト、コールバックなど)
- JavaScriptの特徴
- 非同期処理
- Promiseオブジェクト
- async/await
- 歴史
JavaScriptとは
JavaScript(JS)は、Webページに動きや対話性を持たせるために開発されたプログラミング言語です。HTMLやCSSと組み合わせて使用され、サーバーを介さずブラウザだけでwebページに動的な機能を追加できます。また後述のNode.jsなどの実行環境によりフロントエンド以外の多様な用途でも使われています。
JavaScriptの特徴的な基本構文
変数宣言
JavaScriptは動的型付け言語ですが、以下のようにlet, constを使った変数宣言が必要です。
(現在varは非推奨)
const message = "Hello, JavaScript!"; // 定数(再代入不可。特に理由がなければこちらを使うべし。)
let x = 10; // 変数(再代入可)
var y = 1; // 古い変数の宣言方法(非推奨)
console.log(message); //JavaScriptではconsole.log()でコンソールに文字や変数の値を出力できます
console.log(x);
// Hello, JavaScript!
// 10
関数宣言
関数の定義には複数の書き方があります。まずは普通の関数宣言。
function double(x){return x * 2}; // function 関数名(引数){処理}; もちろん引数は複数も可
上の書き方の関数名のところを省略すると無名関数と呼ばれ、この無名関数を変数に代入することで関数式という宣言の仕方になります。
const double = function(x){return x * 2 }; // function(引数){ 処理 } が無名関数と呼ばれる部分。
そして、無名関数はアロー関数という書き方ができます。アロー関数はthisの挙動でも通常の関数とは違う部分があるので後述します。
const double = (x)=>{return x * 2}; // (引数)=>{ 処理 } 矢印=>で(引数)を{処理}に渡すイメージ。
コールバック関数
ある関数Aの引数として「後で実行したい関数B」を渡し、Aの実行中または実行後に関数Bを実行する(呼び戻す)ことができます。このとき渡された関数Bをコールバック関数と呼びます。
const functionA = (x, callback)=>{
const doubled = x * 2;
return callback(doubled);
};
const functionB = x => x + 1; //アロー関数は処理が1行の式で完結する場合{}とreturnを省略できる。
//また引数が一つの場合()を省略できる
const result = functionA(10, functionB);
console.log(result);
// 21 10 * 2 + 1 = 21
このコードでは関数Aは第一引数10と、引数に1を加える関数Bをコールバック関数を受け取って、引数を二倍した後コールバック関数である関数Bに渡し、その結果を返しています。
オブジェクト
JSのデータタイプの一つにオブジェクト型があります。辞書のような書き方でデータに変数や関数を持たせることができます。オブジェクトが持つキーと値のペアをプロパティと言い、中でも値が関数になっているものはメソッドと言います。
const obj = {
key1: 21, //数値を値にもつプロパティ
key2: "value", //文字列を値に持つプロパティ
key3: x=> x * 2, //関数を値に持つプロパティ(メソッド)
};
console.log(obj.key1);
console.log(obj.key2);
console.log(obj.key3( obj.key1 ));
// 21
// value
// 42
JavaScriptはオブジェクト指向の中でもプロトタイプベースという設計思想を用いています。
また、JavaScriptでは配列や関数もオブジェクトとして振る舞います(詳しくはコンストラクタ関数の項へ)。
関数がオブジェクトであるというのはどういうことでしょうか?
それは関数もまた、プロパティを持つことができるということです。
const sayHello =()=>{
console.log("こんにちは");
}
sayHello.language = "Japanese";
sayHello();
console.log(sayHello.language);
// こんにちは
// Japanese
関数sayHelloにもプロパティを設定できています。
this
thisはコンテキストによって値が異なる特殊なキーワードです。
関数が単独で呼び出された場合、その中のthisはグローバルオブジェクト(実行環境(後述)に依る)、または(strictモードの時は)undifinedを指します。
function showThis(){
console.log(this);
};
showThis();
// Window (実行環境がブラウザならグローバルオブジェクトはWindow, もしNode.jsならglobal)
関数がオブジェクトのメソッドとして呼び出された場合、関数内のthisはその呼び出し元のオブジェクトを指します。
const obj = {
showThis: function(){
console.log(this);
},
name: "Taro",
sayHello: function(){
console.log(`Hello, my name is ${this.name}.`);
// `(バッククオート)で囲むことで文字列の中に変数を${}の形で挿入できます
},
};
obj.showThis();
obj.sayHello();
// obj
// Hello, my name is Taro.
メソッドとして関数を定義する際、アロー関数を使うと、その関数が定義される時点のthisを引き継ぎます。そのメソッドの呼び出し元のオブジェクトではなく、書かれた時点の外側のthisを引き継ぐというのが重要です。
const obj = {
showThis: function(){
console.log(this);
},
showArrowThis: ()=>{
console.log(this); // アロー関数で定義するとメソッドの外側のthisを引き継ぐので
// この場合thisが指すのはグローバルオブジェクト(window)になる
},
objMethod: function(){
const arrow = ()=>{ // メソッドの中でアロー関数を定義
console.log(this); // arrowの外側のメソッド(objMethod)のthisを参照する
// objMethodはobjの(アロー関数でない)メソッドなのでこのthisが指すのはobjになる
};
arrow();
},
};
obj.showThis();
obj.showArrowThis();
obj.objMethod();
// obj
// Window showArrowThisが定義されたスコープでのthisはグローバルオブジェクト
// obj objMethodの中のarrowでのthisはobjなのでobjが出力される
コンストラクタ関数
クラス(オブジェクトの設計図)をもとにオブジェクトを作り出すクラスベースに対して、JSで採用されているプロトタイプベースとは、オブジェクトをもとに別のオブジェクトを作り出す仕組みのことです。
Object.create(prototype);
で引数に渡すオブジェクトprototypeを継承するオブジェクトを生成できます。
// 親オブジェクト(プロトタイプになるオブジェクト)
const animal = {
type: "動物",
speak: function () {
console.log(`${this.name} は「${this.voice}」と鳴く`);
}
};
// 子オブジェクト(animalを継承)
const dog = Object.create(animal); // animalをプロトタイプにするオブジェクトを返してdogに代入
// 子オブジェクトに独自のプロパティを追加
dog.name = "犬";
dog.voice = "ワンワン";
console.log(dog.type); // 動物
dog.speak(); // → 犬 は「ワンワン」と鳴く
プロトタイプベースという仕組みにおいて、オブジェクトの継承などにはコンストラクタ関数が使われることがあります。
コンストラクタ関数を定義するとJS実行エンジンがprototype
という新しいオブジェクトを自動で作成します。
コンストラクタ関数にnewキーワードをつけて実行することでprototype
を継承する新しいオブジェクト(インスタンス)を生成できます。
newキーワードでは以下のような処理が行われています。
- 空のオブジェクトを作成 const obj = {};
- コンストラクタ関数のprototypeを空のオブジェクトに適用
このときコンストラクタ関数内のthisは1で作成した空のオブジェクトを指すように指定している - その作ったオブジェクトを返す(もしreturnが明示されている場合はそれを返す)
function Cat(name){ //コンストラクタ関数の関数名の頭文字は大文字にします
this.name = name;
}
const catA = new Cat("Mike"); //Catのプロパティを継承するオブジェクトを生成しCatの処理をして返す
console.log(catA.name);
// Mike
現在はクラスベースで書く構文も追加されています。
JavaScriptの実行環境
実行環境とは
私たちが普段書いているJavaScriptのコードは、単なるテキストにすぎません。それ自体には動作する力はなく、それを解釈して動かしてくれる場所(=実行環境)が必要です。
実行環境は以下のような役割を果たします
- JavaScript実行エンジンがコードを解釈して実行する
- 必要に応じてOSやブラウザの機能を使わせてくれる
- 非同期処理を安全に制御する仕組み(イベントループなど)を提供する(非同期処理については後述)
つまり、実行環境はJavaScriptが「意味のある動き」をするための舞台装置なのです。
Node.js
Node.jsは、もともとGoogleChromeに搭載されていたJavaScript実行エンジンであるV8をサーバーサイドに搭載したJSの実行環境です。Node.jsの登場によって、それまではフロントエンドで使用されていた言語だったJavaScriptで、サーバーサイドを処理(サーバーを立てたり、ファイルを操作したり、データベースとやりとりしたり)することが可能になりました。
シングルスレッド
ブラウザやNode.jsを含む多くのJS実行環境ではシングルスレッドが採用されています。
シングルスレッドとは、一つのプロセス内で1つの命令しか実行できない状態です。タスクを1つずつ順番に処理していきます。
プログラムの実行順序が直線的で予測しやすいため、開発者にとって理解しやすく、デバッグも容易です。
一方で、一度に一つの処理しか実行できないため、時間のかかる処理(ファイルの読み込みやネットワーク通信など)があると他の処理が待たされるため、画面がフリーズしたり、操作に反応しなかったりするなどユーザーの体験に悪影響を及ぼす可能性があります(スレッドが占有された状態はブロッキングと呼ばれています)。
API
API(Application Programming Interface)とは、あるソフトウェアが他のソフトウェアやサービスの機能を利用するためのインターフェースです。あらかじめ定義された関数などを用いて簡単にほかのソフトウェアが提供している機能を利用することができます。
例えばconsole.log()
もAPIの一種です。実行環境内部で処理が実装されており、jsファイル内でconsole.log()
メソッドを呼び出すことでその機能を利用できます。
非同期処理
JSの実行環境には前述のシングルスレッドの問題に対応するため、非同期APIとイベントループという仕組みが備わっています。これらを使うことでJavaScriptはシングルスレッドでありながらも、あたかも複数の処理が同時に動いているかのような疑似的な並列処理(非同期処理)を実現できます。
非同期API
非同期APIとは、処理の完了を待たずに、実行環境に処理を任せておき、プログラムを先に進められるように設計されたAPIのことです。
これにより時間のかかる処理を実行しても、その完了を待つことなく次の処理へスムーズに進むことができる、つまり非同期処理を行うことができます(例えば、裏でファイルの読み込みを進めつつ、ユーザーの画面操作に反応することができます)。
イベントループ
まずは「What the heck is the event loop anyway?」を見てイメージをつかんでください。
イベントループはJS実行環境が提供していおり、以下の図のような仕組みになっています。ひとつずつ解説していきます。
コールスタック
コールスタックは関数の呼び出し履歴を管理する場所です。スタックというデータ構造をとっています。
const sayHello = ()=>{
console.log("Hello");
};
const main = ()=>{
sayHello();
};
main();
この時のコールスタックは呼び出された関数を順にコールスタックに積んでいきます(関数の定義はsayHello, mainの順番ですが呼び出し順はmainが最初であることを確認してください)。
- main() が呼び出される
空のコールスタックに main が積まれる。
→ Stack: [main] - main 関数の中で sayHello() が呼び出される
コールスタックに sayHello が積まれる。
→ Stack: [main, sayHello] - sayHello の中の console.log("Hello") が実行される
→stack: [main, sayHello, console.log] - "Hello" と出力されconsole.log()の処理は終了する。
console.logはスタックから取り除かれる。
→stack: [main, sayHello]
sayHello の処理が終わり、スタックから取り除かれる。
→ Stack: [main] - main のすべての処理が終わり、スタックから取り除かれる
→ Stack: [](空)
今回のコードのように関数がネストしている場合、スタック構造でないと制御フローが破綻してしまいます。
WebAPI
非同期APIは実行環境に処理を任せてプログラムを先に進められるAPIと述べました。もう少し詳しく見ていきましょう。
setTimeout()
も非同期APIの一つです。関数の形で利用できる非同期APIは非同期関数と呼ばれることもあります。setTimeout関数は指定した遅延時間(ミリ秒)の後に関数やコードを実行する非同期関数です。第一引数にコールバック関数を、第二引数に遅延時間をミリ秒で取ります。
const main =()=>{
console.log("Start");
setTimeout(()=>{
console.log("Hello");
}, // ここではコールバック関数を無名関数として定義しながら非同期関数に引数として渡しています。
1000);
console.log("End");
};
main();
// Start
// End
// (1秒間空く)
// Hello
このとき出力が、start→(1秒間空く)→Hello→Endとならないのはなぜでしょうか?
それは先程述べたように、非同期処理ではある処理を実行環境に任せて(この非同期処理を担当する部分はWebAPIとも呼ばれます)、その完了を待たずに次の処理へ進むからです。今回の場合setTimeoutという非同期関数がコールスタックに積まれたあと、すぐにWebAPIにその処理を任せ、スタックからは取り除かれます。WebAPIが任されたsetTimeoutの処理(指定ミリ秒待つ)をしている間、メインのスレッドで次の処理console.log("End")が進行し、1秒後にようやくHelloが出力されています。
WebAPIには非同期処理とともにコールバック関数(cb)が渡されており、非同期関数の処理が完了した後コールバック関数をタスクキューに追加します。
コールバック関数をとる非同期関数はsetTimeoout()
のほかにもsetInterval()
やaddEventListener()
、Node.jsのfs.readFile()
などほかにも存在します。
タスクキュー
非同期関数が処理をWebAPIに任せていることはわかりました。setTimeoutでは指定ミリ秒待つという処理をWebAPIが行っています。しかし実はこの関数に渡されたコールバック関数の処理は、WebAPIは実行していません。非同期関数に渡したコールバック関数の行方はどうなっているのでしょうか。
関数がWebAPIの処理を終えた後、非同期関数の引数に渡したコールバック関数は非同期タスクキューに追加されています。
タスクキューはその名の通りキューというデータ構造をしています。
JavaScriptの実行環境ではイベントループがタスクキューを監視し、コールスタックが"空になったタイミングで"タスクキューからタスクを取り出し、コールスタックに積んで実行します。
console.log("Start");
setTimeout(()=>{
console.log("Hello");
}, 1000);
setTimeout(()=>{
console.log("Bye");
}, 1000);
console.log("End");
// Start
// End
// (1秒間空く)
// Hello
// Bye
このコードで二つのsetTimeout関数はメインスレッドで連続して実行され、それぞれがほぼ同時に1秒間待機した後タスクキューに追加されます。そしてEndが出力された後、連続して二つのコールバック関数が実行されます(HelloとByeの間は1秒間空きません。タイマーは別々で1秒をカウントしています)。
つまり非同期処理に渡されたコールバックはメインスレッドの処理の後に実行されているということです。
マイクロタスクキューの説明はPromiseの項で行います。
Call Back Hell
先程のコードのようにsetTimeoutを二つ並べただけだと二つの非同期処理は別々に処理されます。
では前の非同期処理を終えてからつぎの非同期処理を行いたいときはどうすればよいでしょうか?
console.log("Start");
setTimeout(()=>{
console.log("Hello");
setTimeout(()=>{
console.log("Bye");
setTimeout(()=>{
console.log("Good night");
}, 1000)
}, 1000);
}, 1000);
console.log("End");
// Start
// End
// (1秒間空く)
// Hello
// (1秒間空く)
// Bye
// (1秒間空く)
// Good night
このコードを実行すると、前のメッセージの出力を待ってからつぎのsetTimeoutが走り出すので、1秒おきにメッセージが出力されます。
しかし、ネストが深くなってしまい可読性の低いコードになってしまいます。
これをコールバック地獄(Call Back Hell)と呼びます。
Promise
前述のコールバック地獄の問題を解決するために、Promiseというオブジェクトが開発され、ネストを減らして連続する非同期処理を書くことができるようになりました。
Promise
はJavaScriptの標準オブジェクトです。(ECMAScriptの仕様に含まれている)
Promise関数
Promise()
はコンストラクタ関数であり、newキーワードとつけて使用することでPromiseオブジェクト(Promiseインスタンス)を生成できます。こちら
Promiseオブジェクトはつぎの3つのState(状態)があります
- Pending(処理の待機中)
- fulfilled(成功)
- rejected(失敗)
これらは内部スロットというところで保持されています。(直接アクセスすることはできません)
Promise関数の引数にはexecutorと呼ばれる関数を渡します。new Promise(executor);
このexecutor関数はよくアロー関数などを用いて引数の中で定義され、resolve()
とreject()
という二つの関数を引数に取ります。resolve()
とreject()
もJavaScriptの標準関数です。
execxutorとresolve関数とreject関数
まずreject関数の説明をします。
reject(reason) は、JSに標準で組み込まれている、非同期処理が失敗したときに呼び出す関数です。引数には、失敗の理由やエラーオブジェクトなどを渡します。この関数を呼ぶと、Promiseの状態が "pending" から "rejected" に変わり、catch() でその引数を受け取って処理することができます。
つぎにresolve関数です。resolveの処理は引数がPromiseオブジェクトか否かによって変わります。
resolve(value) の引数にPromiseオブジェクトでない値を入れる場合は、非同期処理が成功したことを表します。resolveを呼び出したPromiseオブジェクトの状態が "pending" から "fulfilled" に変わり、引数に渡した成功時の値をthen() に引き渡します。
しかし、resolveの引数にPromiseオブジェクトを渡した場合は、呼び出したPromiseオブジェクトの状態は引数のPromiseオブジェクトの状態と値を引き継ぎます。これは同化と呼ばれます。
executor関数はresolve関数とreject関数を引数に取ります。Promiseが生成された直後に自動的に実行され、Promiseオブジェクトの処理の本体となります。その中で、成功したら resolve()
,失敗したら reject()
を呼び出すことでPromiseの状態を変え、処理の成功または失敗を伝えます。
console.log("Start");
const promiseObj = new Promise((resolve, reject)=>{
const success = true;
if (success){
console.log("succeed");
resolve();
}
else{
console.log("failed");
reject();
}
}); // executorはnewでPromiseオブジェクトを生成すると即時に実行されるので
// Promiseオブジェクトはすぐにresolveされ、状態はfulfilledになります
console.log("End");
// Start
// succeed 今回はsuccess = true;と定義したので常に成功
// End
then, catch, finally メソッド
Promiseオブジェクトはthen
, catch
, finally
というメソッドを持ちます。Promiseによる"非同期"な処理はこれらのメソッドとマイクロタスクキューによって行われています。それぞれのメソッドは非同期で実行したいコールバック関数を引数に取ります。
thenにはPromiseオブジェクトがfulfilledの状態のときに実行したい処理を渡します。
catchにはPromiseオブジェクトがrejectedの状態のときに実行したい処理を渡します。
finallyにはPromiseオブジェクトがfulfilledでもrejectedでも必ず実行させたい処理を渡します。
Promiseオブジェクトの状態がPendingの時はどれも実行されません。
これらのメソッドは全て返り値として新しいPromiseオブジェクトを返します。
const promiseObj = new Promise((resolve, reject)=>{
const success = true;
if (success){
resolve("succeed");
}
else {
reject("failed");
}
});
console.log("Start");
promiseObj
.then((result)=>{
console.log(result);
})
.catch((error)=>{
console.log(error);
})
.finally(()=>{
console.log("finally")
})
console.log("End");
// Start
// End
// succeed PromiseObjの状態はfulfilledなのでthenの引数の処理だけが非同期に実行されます。
// finally
resolveの引数にPromiseオブジェクトを渡す場合は以下のようになります。(同化の場合)
const promiseObjA = new Promise((resolve, reject)=>{
const success = false;
if (success){
resolve("succeed");
}
else {
reject("failed");
}
}); // 今回の場合、PromiseObjAの状態は即時rejectedに変わります。
const promiseObjB = new Promise((resolve, reject)=>{
const promise = promiseObjA;
resolve(promise);
}); // 今回の場合、PromiseObjBの状態はpromiseObjAと同じになります
console.log("Start");
promiseObjB.then((result)=>{
console.log(result);
}).catch((error)=>{
console.log(error);
});
console.log("End");
// Start
// End
// failed
// PromiseObjBの状態はpromiseObjAの状態 rejected("failed") を引き継ぎcatchの処理を実行します
マイクロタスクキュー
通常の(タスクベースの非同期な)WebAPIでは処理を終えた後、非同期関数の引数に渡されたコールバックはタスクキューに追加されますが、Promiseのメソッド(.then, .catch, .finally)に渡されたコールバックはマイクロタスクキューに追加されます。
またマイクロタスクキューはタスクキューよりも実行の優先順位が高いです。
具体的には以下のような流れで処理されています。
- JavaScriptの**同期処理(通常の関数など)**が コールスタック に積まれて、順番に実行されていきます
- コールスタックが完全に空になったあと、イベントループはすぐにマイクロタスクキューを確認し、キューの先頭から順番に"すべて"実行します
- マイクロタスクが全部終わった後、ようやく**マクロタスク(タスクキュー)**にある処理が"1つだけ"取り出されてコールスタックに積まれ実行されます
- イベントループはまた1~3の順番で繰り返し処理します
Promiseの.then()後の処理がマイクロタスクキューに入るのは、これらはできるだけ早く、かつ同期処理が終わった直後に実行されるべき非同期処理だからです。
Promiseでの非同期処理
new Promise()を即座に返す関数を作ることでPromiseオブジェクトを用いた非同期関数のようにふるまう関数を作ることができます。(タスクキューよりも優先度が高い)
function promiseFunc(){
return new Promise((resolve)=>{ // 引数rejectは省略できます
console.log("fulfilled");
resolve();
});
}
console.log("Start");
promiseFunc().then(() => console.log("asynchronous"));
console.log("End");
// Start
// fulfilled fulfilledはここで出力され
// End
// asynchronous asynchronousはここで出力されます。コードの実行の流れについて以下で解説します
このコードではPromiseFuncがPromiseオブジェクトを返しています。このオブジェクトの引数に渡したexecutorの処理であるconsole.log("fulfilled");
とresolve();
は即時実行されます(fulfilledが出力され、Promiseオブジェクトの状態はfulfilledに変わります。)。
状態がfulfilledなのでthenのコールバックがマイクロタスクキューに登録されます。
コールスタックの最後の処理console.log("End")
が実行され、コールスタックが空になるとマイクロタスクキューの処理() => console.log("asynchronous")
をコールスタックに積んで実行します。
このような流れで上の出力が得られました。
下のようにexectorにタスクベースの非同期関数を書いて(Promiseでラップして)Promiseを返す非同期処理として扱うこともできます。
const promiseSetTimeout = message=>{
return new Promise((resolve)=>{
setTimeout(()=>{
console.log(message);
resolve(); // 非同期処理が完了してから最後にresolveを呼ぶ
}, 1000);
})
console.log("Start");
promiseSetTimeout("Hello")
.then(() => promiseSetTimeout("Bye")) // then(promiseSetTimeout("Bye"))は間違い
.then(() => promiseSetTimeout("Good night"));
console.log("End");
// Start
// End
// (1秒間空く)
// Hello
// (1秒間空く)
// Bye
// (1秒間空く)
// Good night
先程のCall Back Hellのコードと処理の内容は同じですがネストせずに書いている分可読性が向上しています。このようにthenで返されたPromiseオブジェクトにthenをつなげることを繰り返し、連続的な非同期処理を行うやり方はPromiseチェーンと呼ばれます。
Promiseの仕様の詳細はこちらの記事
async/await
先程のようなPromiseオブジェクトを返す(Promise-basedな)関数はasyncキーワードを使って宣言すると、Promiseチェーンを使わずに、よりきれいに非同期処理を書くことができます。
asyncキーワードを使って宣言した関数はreturnする値を明示的にPromiseにしなくても、Promiseオブジェクトにラップして返されます。
function promiseFunc(){
return new Promise((resolve)=>{
resolve("Hello");
});
}
promiseFunc()
.then((value)=> console.log(value));
// Hello resolveされた値Helloをthenによって非同期処理で出力
async function asyncFunc(){ // 上のコードと同じ関数をasyncで宣言。よりシンプルに書ける
return "Hello";
}
asyncFunc()
.then((value)=> console.log(value));
// Hello
前述の通りPromiseではexectorは同期的に処理され、thenメソッドに渡すコールバックがマイクロタスクキューに追加されます。
async/awaitではawaitキーワードのあとに書いた処理のつぎ以降の処理がマイクロタスクキューに追加されます(awaitまでの処理は同期的に処理されます)。
function promiseFunc(){
return new Promise((resolve)=>{
console.log("fulfilled");
resolve();
});
}
console.log("Start");
promiseFunc().then(() => console.log("asynchronous"));
console.log("End");
// Start
// fulfilled
// End
// asynchronous
async function asyncFunc(){
await console.log("fulfilled");
console.log("asynchronous");
}
console.log("Start");
asyncFunc();
console.log("End");
// Start
// fulfilled
// End
// asynchronous
awaitキーワードの右辺が値などの場合、awaitは即時にその値を返しますが、awaitの右辺がPromiseオブジェクトの場合、awaitはそれが"fulfilled"になるまでasync関数内の処理の進行を止めて待ちます(async関数外の処理はメインスレッドで進行します)。
const promiseSetTimeout = message=>{
return new Promise((resolve)=>{
setTimeout(()=>{
console.log(message);
resolve();
}, 1000);
});
}
async function asyncFunc(){
console.log("Before await");
await promiseSetTimeout("Hello"); // ここで返されるPromiseがfulfilledになるのを待つ
console.log("After await"); // fulfilledになってからこれがマイクロタスクキューに追加される
}
console.log("Start");
asyncFunc();
console.log("End");
// Start
// Before await
// End
// Hello
// After await
歴史(おもろい)
私たちがwebサービスを利用する際にはクライアントサーバモデルという仕組みが使われています。JavaScriptが登場する以前、HTMLとCSSのみで書かれたwebサイトでは、ページが更新されるたびにサーバーと通信してページ全体を再読み込みする必要がありました。
1995年、Netscape NavigatorとInternet Explorerによる第一次ブラウザ戦争のさなか、Webサイトでもデスクトップアプリケーションのように、通信を待たずユーザー操作へ即座に反応するUIを実現するためにJavaScriptは開発されました。
高速かつ簡潔にブラウザを制御するために、シングルスレッド、イベントループ、プロトタイプベースといった仕組みが採用され、軽量かつ反応の良い設計が特徴です。また、初心者にも扱いやすい構文を持つことから、幅広い層に支持される言語となりました。
その後、1997年からJavaScriptはECMAScriptとして規格が標準化され、ブラウザ間での互換性が向上しました。
1998年にはDOMによりJavaScriptからHTMLを操作できるようになり、1999年にはXMLHttpRequestにより非同期通信が可能となりました。
2005年、これらを組み合わせたAjax(Asynchronous JavaScript And XML)が登場。画面遷移をせずにページの一部を動的に書き換えられるようになり、GmailやGoogle MapsのようなWebアプリの実現を後押しした。
2008年には、JITコンパイル方式を採用した高速なJavaScript実行エンジン「V8」が登場。
2009年には、そのV8エンジンをブラウザ外でも使えるようにしたNode.jsが登場。
Node.jsはイベントループを活用した非同期I/Oを基本にしているため、大量の同時接続を効率的に処理できます。また、パッケージ管理ツールのnpmにより、ライブラリを簡単に利用・管理できるようになりました。
2015年にはES2015(通称ES6)がリリースされ、let / const、アロー関数、クラス構文、Promise、モジュールなどが追加されました。
さらに2017年にはES2017でasync/awaitが導入され、非同期処理の記述がより簡潔になりました。
近年では、JavaScriptを静的型付け言語として拡張し、大規模開発にも対応できるTypeScriptの人気が高まっています。
参考
- 『イベントループとプロミスチェーンで学ぶJavaScriptの非同期処理』
こちらの本は非常に参考にさせていただきました。これを読み進めるために必要なことを学び、ここにまとめたつもりなのでこの記事を読んだ方はこちらに進むことをお勧めします。