JavaScript の習得が遅々として進まない自分には、昔は無かった class とか => とか、さらには非同期関数とかが出てきて、Q&A サイトなどでの話題についていけない状況にあります。(汗)
というわけで、JavaScript の非同期についてちょっとだけ勉強してみました。以下に勉強したことを忘れないように書いておきます。
まず、非同期関数ですが、MDN のドキュメント「非同期関数」の説明が自分的には分かりやすいと思いました。
そのドキュメントによると "非同期関数は async キーワードで宣言され、その中で await キーワードを使うことができます" ということで、ドキュメントのサンプルコード(下に転載します)の asyncCall を非同期関数と呼ぶようです。
function resolveAfter2Seconds() {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}
async function asyncCall() {
console.log('calling');
const result = await resolveAfter2Seconds();
console.log(result);
// expected output: "resolved"
}
asyncCall();
非同期関数の利点は、"async および await キーワードを使用することで、プロミスベースの非同期の動作を、プロミスチェーンを明示的に構成する必要なく、よりすっきりとした方法で書くことができます" ということだそうです。
ということで、上で言う「プロミスベースの非同期の動作」すなわちコードに出てくる Promise, resolve が JavaScript における非同期関数の核心のようです。
まず、Promise というのは何かですが、自分的に分かりやすかったのは「現代の JavaScript チュートリアル」の Promise と「JavaScript Primer」の [ES2015] Promise という記事でした。(MDN のドキュメント「プロミスの使用」も読みましたがほとんど分かりませんでした)
記事に書いてあったことを要約して以下に書きます。(解釈が間違っているところもあるかも)
Promise は非同期処理の最終的な完了もしくは失敗を表すオブジェクトで、基本的に以下のように作成します。
let promise = new Promise((resolve, reject) => {
// 処理
});
上の Promise コンストラクタの引数に渡している関数 (resolve, reject) => { // 処理 } は executor と呼ばれる関数で、resolve と reject という 2 つの引数を取ります。
resolve と reject は JavaScript に定義済みのコールバックで、以下のように executor の結果に応じていずれかを呼び出す必要があります。
-
resolve(value) – 正常に終了した場合。Promise の result プロパティを value に設定。
-
reject(error) – エラーが発生した場合。Promise の result プロパティを error に設定。
具体例は上のコードの resolveAfter2Seconds 関数を見てください。Promise オブジェクトを生成してそれを戻り値として返すようにしています。
asyncCall 関数が resolveAfter2Seconds 関数を呼ぶと、resolveAfter2Seconds 関数は Promise オブジェクトを生成して即座に戻り値として返します。
Promise コンストラクタの引数の executor 関数は new Promise の時点で自動的かつ即座に実行されます。
executor 関数内の setTimeout により 2 秒後に resolve('resolved') が呼ばれて、Promise オブジェクトの result プロパティは 'resolved' という文字列に設定されます。
asyncCall 関数内で resolveAfter2Seconds 関数に await が付与されているところに注目してください。await よって、resolveAfter2Seconds が戻り値として返した Promise が resolve されるまで次の行の console.log(result); には進まず待機します。
さらに await によって Promise オブジェクトの result プロパティのオブジェクトが取り出されて渡されます。上のコード例では resolve('resolved') としているので 'resolved' という文字列が渡されます。
そのあたりは C# の非同期処理で使う Task<T>, async, await と同様なようです。
実際に使ってみると理解が深まると思って、先の記事「canvas の画像をアップロード (その 2)」の JavaScript のコードに非同期関数を適用してみました。そのコードを載せておきます。
input type="file" でファイルの選択が完了すると発生する change イベントのリスナを非同期関数式にして、その中で Promise を使った関数 CreateDataUrl と CreateImage を await を付与して呼び出すようにしています。詳しくはコード内のコメントを見てください。
@{
ViewData["Title"] = "Canvas2";
}
<h1>Canvas2</h1>
<input type="file" id="file1" />
<input id="button1" type="button" value="Upload" style="display:none;" />
<br />
<div id="msg"></div>
<canvas id="mycanvas"></canvas>
<div id="result"></div>
@section Scripts {
<script type="text/javascript">
//<![CDATA[
const maxFileSize = 500000;
const allowedContentType = "image/jpeg";
const maxWidth = 500;
const maxHeight = 500;
let inputFile, uploadButton, msgDiv, myCanvas, resultDiv;
// input type="file" から取得できる File オブジェクトを引数
// に渡す。FileReader オブジェクトを生成して readAsDataURL
// メソッドで Data url を生成し、resolve コールバックの引数
// に設定する
const CreateDataUrl = file => {
return new Promise(resolve => {
const reader = new FileReader();
reader.addEventListener('loadend',
e => resolve(e.target.result));
reader.readAsDataURL(file);
});
};
// Data url を引数に渡す。Image オブジェクトを生成しその
// src に Data url を代入。ロード完了後の Image オブジェ
// クトを resolve コールバックの引数に設定する
const CreateImage = dataUrl => {
return new Promise(resolve => {
const img = new Image();
img.addEventListener('load',
e => resolve(e.target));
img.src = dataUrl;
});
};
// DOMContentLoaded イベントで初期設定を行う
window.addEventListener('DOMContentLoaded', () => {
inputFile = document.getElementById("file1");
uploadButton = document.getElementById("button1");
msgDiv = document.getElementById("msg");
myCanvas = document.getElementById("mycanvas");
resultDiv = document.getElementById("result");
if (window.File && window.FileReader && window.FileList) {
uploadButton.addEventListener('click', uploadImage);
// リスナを非同期関数式にして、リスナの中で Promise
// を使った関数 CreateDataUrl と CreateImage を
// await を付与して呼び出す。
inputFile.addEventListener('change', async () => {
resultDiv.innerText = "";
if (ClientValidate(inputFile) == false) {
uploadButton.style.display = "none";
myCanvas.style.display = "none";
return;
}
// 選択された画像の Data url を取得
let dataUrl =
await CreateDataUrl(inputFile.files[0]);
// 画像をロードした Image オブジェクトを取得
let img = await CreateImage(dataUrl);
// canvas に画像を描画
DrawImageOnCanvas(img);
});
}
else {
inputFile.style.display = "none";
myCanvas.style.display = "none";
msgDiv.innerText =
"File API がサポートされていません。";
}
});
// 選択されたファイルの検証のためのヘルパ関数
function ClientValidate(fileUpload) {
msgDiv.innerText = "";
if (fileUpload.files[0] == null) {
msgDiv.innerText = "ファイルが未選択です。";
return false;
}
if (fileUpload.files[0].type != allowedContentType) {
msgDiv.innerText = "選択されたファイルのタイプが " +
allowedContentType + " ではありません。";
return false;
}
if (fileUpload.files[0].size > maxFileSize) {
msgDiv.innerText = "ファイルのサイズが " +
maxFileSize + " バイトを超えています。";
return false;
}
return true;
}
// 画像が読み込み済みの Image オブジェクトを引数に受け取る。
// その画像を指定されたサイズに縮小して canvas に描画
function DrawImageOnCanvas(image) {
// オリジナル画像のサイズ
const w = image.width;
const h = image.height;
let targetW, targetH;
const context = myCanvas.getContext('2d');
if (w <= maxWidth && h <= maxHeight) {
// w, h ともに制限 maxWidth, maxHeight 以内 ⇒
// そのままのサイズで canvas に描画
myCanvas.setAttribute('width', w);
myCanvas.setAttribute('height', h);
context.drawImage(image, 0, 0);
}
else if (w < h) {
// w, h どちらかが制限オーバーで h の方が大きい ⇒
// 高さを maxHeight に縮小
targetH = maxHeight;
// 幅は高さの縮小比率で縮小
targetW = Math.floor(w * targetH / h);
myCanvas.setAttribute('width', targetW);
myCanvas.setAttribute('height', targetH);
context.drawImage(image, 0, 0, targetW, targetH);
}
else {
// w, h どちらかが制限オーバーで w の方が大き�� ⇒
// 幅を maxWidth に縮小
targetW = maxWidth;
// 高さは幅の縮小比率で縮小
targetH = Math.floor(h * targetW / w);
myCanvas.setAttribute('width', targetW);
myCanvas.setAttribute('height', targetH);
context.drawImage(image, 0, 0, targetW, targetH);
}
uploadButton.style.display = "inline-block";
myCanvas.style.display = "block";
}
// canvas の画像データを DataURL 形式で取得し、JSON 文
// 字列を組み立てて fetch API でサーバーに送信する
async function uploadImage() {
const context = myCanvas.getContext('2d');
const dataUrl = context.canvas.toDataURL("image/jpeg");
const params = {
method: "POST",
body: '{"imgBase64":"' + dataUrl + '"}',
headers: { 'Content-Type': 'application/json' }
}
const response = await fetch("/api/Canvas", params);
if (response.ok) {
const message = await response.text();
resultDiv.innerText = message
} else {
resultDiv.innerText = "アップロード失敗";
}
}
//]]>
</script>
}
非同期関数がある程度理解できれば、先の記事「canvas の画像をアップロード (その 2)」のコードに比べてかなり読みやすくなったような気はします。