同期処理と非同期処理
あるプログラムが処理の途中で関数を呼び出して実行するとき以下の処理方法の違いがある。
同期処理は呼び出し先の関数の処理が完了した後に呼び出し元の次の処理を実行する。
非同期処理は呼び出し先の関数の処理の完了を待たずに呼び出し元の次の処理を実行する。
また、通信を行う 2 つの主体があるとき、データの送信処理と受信処理のタイミングを合わせる仕組みをもつのが同期通信で、タイミングを合わせる仕組みをもたないのが非同期通信である、という解説もある。
英語では、synchronous が「同期性の」で、asynchronous が「非同期性の」を意味する。つまり非同期はシンクロの対概念である。
人がやることに例えると、電話は同期コミュニケーションでメールは非同期コミュニケーション。
JavaScript の非同期処理
JacaScript はシングルスレッドで動作する。非同期処理もメインスレッドで実行される。メインスレッドで同期処理と非同期処理が並⾏処理(concurrent)として扱われ、同期処理と非同期処理を切り替えながら実行される。非同期処理の実行タイミングはメインスレッドで実行されている同期処理の影響を受ける。時間のかかる同期処理があればそれが完了した後に非同期処理が実行される動きになる。例えば setTimeout 関数で 1000 ミリ秒後にコールバック関数を実行する非同期処理が呼び出されたとき、1000 ミリ秒後にはキュー(待ち行列)に処理が登録はされるが、実行はメインスレッドの他の処理が完了してからになる。1000 ミリ秒に指定したら、1000 ミリ秒後に実行されるというわけではない。
もし⾮同期処理が別スレッドで⾏われるならば、⾃由なデータへのアクセスは競合状態(レースコンディション)を引き起こしてしまうかもしれない。
クライアントサーバシステムにおける非同期処理
Ajax
クライアントサーバシステムにおいて、現在では JavaScript による非同期通信はあたりまえに行われていて避けて通れない。同期通信ではブラウザはサーバー側で処理が完了するのを待たなければ処理を継続できない。また、ブラウザがページ全体を更新する処理が発生してしまう。そのため、操作性やパフォーマンスに課題があった。一方、非同期通信ではブラウザはサーバー側での処理が完了するのを待たずに処理を実行できる状態になる。また、ブラウザはページ全体を更新せず JavaScript がサーバとの通信を行いページの必要な部分のみを動的に変更することができる。そのため、操作性やパフォーマンスが同期通信に比べて向上する。
クライアントサイドからなにかしらのWeb APIにリクエストし、応答結果のデータを受けとってページを更新したりするのは一般的な機能になっている。Ajaxと呼ばれる技術によって一般的になった。Ajax は非同期で JavaScript がサーバーと通信を行い得た結果をドキュメントに反映することができる技術である。データ形式はJSON、XML がよく使われる。
JavaScript で非同期通信を行う方法
Fetch メソッドはネットワークリクエストを行いサーバからデータを取得できるモダンで基本的な方法である。
XHR オブジェクト。通信プロトコルは HTTP に限定されるわけではない。XMLデータだけ扱うわけではない。やや非推奨だが必要なケースもある。
基本的な原則として、異なるドメインへのリクエストが制限される(クロスドメイン制限)。クロスオリジンリクエストはブラウザの機能によって拒否される。
しかし異なるドメインへのリクエストを行うことはよくあるのでクロスドメイン制限を回避する方法はいくつかある。
- サーバー側でクロスオリジンリクエストを明示的に許可する。Access-Control-Allow-Origin 応答ヘッダーを使う手法。
- JSONP。リクエスト用の script タグを生成して、応答結果を処理できるコールバック関数を用意する手法。
- 同一ドメインに配置したプロキシ(サーバーサイドアプリ)から異なるドメインにリクエストする手法。(あまり使用されない?)
非同期通信はライブラリやフレームワークが提供している方法があるのでそれを使うこともできる。非同期通信に向いているJSライブラリにaxiosがある。fetchメソッドより改善されたコードで書ける。
複数の非同期処理を実装する場合にコールバック地獄に陥ることがある。ライブラリに頼らず JS でこの問題を回避することができる。Promise オブジェクトを利用すると複数の非同期処理を簡潔な記法で実装できる。Promise オブジェクトは非同期処理の状態を監視したり、成功または失敗の場合のコールバック関数を定義したりする。
非同期処理を直列で連結するのか並列で処理するのかという問題もある。Promise の all メソッドで並列処理ができる。すべての非同期処理が成功したら何かするような処理の実装になる。不必要に直列処理にしないこと、例えば画像を順序に関係なく非同期に読み込むなら並列処理にするべき。
JavaScript には setTimeOut メソッドなど外部リクエストを行わないが非同期で処理が行われるメソッドもある。
async/await 構文と Promise 構文がある。前者の方が簡潔に書ける。
エラーファーストコールバック
// 参考ページ https://jsprimer.net/basic/async/
// 同期処理で例外をキャッチする例
try {
throw new Error('同期処理のエラー。最も基本的な例外処理。');
} catch (e) {
console.log(e.message);
}
// 非同期処理内の例外処理ができない例
try {
setTimeout(() => {
// throw new Error(
// '非同期処理のエラー。これはキャッチできない。tryブロックの外で実行されるので。'
// );
}, 1000);
} catch (e) {
console.log(e.message);
}
// 非同期処理内で例外処理できるが、外では例外をキャッチできない例
setTimeout(() => {
try {
throw new Error(
'非同期処理のエラー。これは非同期処理の中でキャッチできるが、非同期処理の外からはわからない。'
);
} catch (e) {
console.log(e.message);
}
}, 2000);
// エラーファーストコールバックで非同期処理の成功を伝える例。
dummyFetch('success/', (error, response) => {
if (error) {
console.log(
'非同期処理で例外が発生しました。callback関数の引数にエラーオブジェクトを渡たされています。'
);
console.log(error.message);
} else {
console.log('非同期処理が成功しました。');
console.log(response);
}
});
// エラーファーストコールバックで非同期処理の例外をキャッチする例
dummyFetch('failure/', (error, response) => {
if (error) {
console.log(
'非同期処理で例外が発生しました。callback関数の引数にエラーオブジェクトを渡たされています。'
);
console.log(error.message);
} else {
console.log('非同期処理が成功しました。');
console.log(response);
}
});
asyncTowCallBack(
'failure/',
(response) => {
console.log(response);
},
(error) => {
console.log(error.message);
}
);
/**
* 非同期処理の結果を返すダミーダミー関数です。
* @param path
* @param callback エラーファーストコールバック。第一引数にエラーオブジェクト、第二引数以降にデータを受け取る関数です。
*/
function dummyFetch(path, callback) {
setTimeout(() => {
if (path.startsWith('success/')) {
callback(null, { body: 'レスポンスの本文です。' });
} else {
callback(new Error('データの取得に失敗しました。'));
}
}, 3000);
}
/**
* 非同期処理の成功・失敗それぞれのコールバックを受け取るサンプル関数。
* @param {*} path
* @param {*} successCallback 成功した場合のコールバック関数
* @param {*} failureCallBack 失敗した場合のコールバック関数
*/
function asyncTowCallBack(path, successCallback, failureCallBack) {
setTimeout(() => {
if (path.startsWith('success/')) {
const response = { body: 'レスポンスの本文です。' };
successCallback(response);
} else {
failureCallBack(new Error('データの取得に失敗しました。'));
}
}, 4000);
}
Promise
/**
* 非同期処理を行いPromiseを返すサンプル関数。
* Promiseコンストラクター
* Promiseインスタンスを作成しexecutorと呼ばれる関数を渡す。
* resolveメソッドで成功した場合、rejectメソッドで失敗した場合の通知を行う。
*/
function asyncProcess(path) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (path.startsWith('success/')) {
resolve({
body: `${path}へのリクエストに対するレスポンスの内容です。`,
});
} else {
reject(new Error('非同期処理の例外が発生しました。'));
}
}, 1000);
});
}
// thenでresolveで呼ばれるコールバック、catchでrejectで呼ばれるコールバックを登録
asyncProcess('success/')
.then((response) => {
console.log(response);
})
.catch((error) => {
console.log(error);
});
// catchはthenメソッドのonRejectedと同じ意味のシンタックスシュガー。catchはthenメソッドのエイリアスとも言える。
asyncProcess('failure/')
.then((response) => {
console.log(response);
})
.catch((error) => {
console.log(error);
});
// 構文;p.then(onFulfilled[, onRejected]);
// thenの引数に成功コールバック、失敗コールバックを登録する構文もある。
asyncProcess('failure/').then(
(value) => {
console.log(value);
},
(reason) => {
console.log(reason);
}
);
// 成功時の処理のみ
function delay(timeoutMs) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`${timeoutMs}ミリ秒後に解決されました`);
}, timeoutMs);
});
}
delay(2000).then((response) => {
console.log(response);
});
// 失敗時の処理のみ
function errorPromise(message) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error(message));
}, 2000);
});
}
// then(undefinde, onRejectedCallBack)と意味は同じだがcatchを使うのが推奨。
errorPromise('失敗したときだけエラーをキャッチします。').catch((error) => {
console.log(error.message);
});
// 暗黙のtry...catch文。Promiseコンストラクタ内で例外が発生したら自動的にPromiseは拒否状態になり例外が投げれれる。
// https://ja.javascript.info/promise-error-handling が詳しいエラーハンドリングを説明している。
new Promise((resolve, reject) => {
// throw new Error('例外');
}).catch((error) => {
console.log(error);
});
// Promiseの状態遷移 基本1回状態変化したら後は変化しない性質(settled)
const promise1 = asyncProcess('success/');
console.log(promise1); // fulfilled状態のPromise
const promise2 = asyncProcess('failure/');
promise2.catch((error) => {
console.log(promise2); // rejected状態のPromise
console.log(error);
});
/**
* 1 解決状態のPromiseを作成する
* 2 特定の値で解決されるPromiseを作成する
* 3 解決済みのPromiseにthenで(常に非同期で実行される)コールバックを登録する。
* 4 拒否状態のPromiseを作成する
* 5 拒否状態のPromiseに非同期コールバックを関数を登録する
*/
const fulfilledPromise = Promise.resolve();
console.log(fulfilledPromise);
console.log(Promise.resolve(10));
fulfilledPromise.then(() => {
console.log(
'解決済みのPromiseにコールバック処理を登録。これは常に非同期で実行される。'
);
});
console.log('解決済みPromiseに追加した非同期処理の直後の同期処理');
// const rejectedPromise = Promise.reject(new Error('拒否状態Promiseのエラー'));
// console.log(rejectedPromise);
/**
* Promiseチェーン
*/
// thenはpromiseを返すので続けてthenメソッドを実行できる。
Promise.resolve()
.then(() => {
console.log('promiesチェーンの1回目の成功コールバック');
})
.then(() => {
console.log('promiesチェーンの2回目の成功コールバック');
});
// 1回目でrejected Promiseを返すのでcatchに処理が移る。
Promise.resolve()
.then(() => {
console.log('promiesチェーンの1回目の成功コールバック');
throw new Error('thenメソッド内の例外発生');
})
.then(() => {
console.log('promiesチェーンの2回目の成功コールバック');
})
.catch((error) => {
console.log(error.message);
})
.then(() => {
console.log(
'catchメソッド実行後はfulfilled Promiseを返すのでこのコードは実行される。'
);
return 1;
})
.then((value) => {
console.log(`直前のthenメソッドで返された${value}を受け取って出力する。`);
});
/*
* 成功失敗に関わらず最後に行いたい処理をfinally()で実行する。
* https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally
*/
// 複数の非同期処理の逐次処理 thenメソッドで実行したい順番でつないでいく
const results = [];
asyncProcess('success/data_1')
.then((response) => {
results.push(response.body);
return asyncProcess('success/data_2'); // Promiseを返して次のthenで処理する
})
.then((response) => {
results.push(response.body);
return asyncProcess('success/data_3');
})
.then((response) => {
results.push(response.body);
})
.then(() => {
console.log(results);
});
// 複数の非同期処理の並列処理。promise.all()を使う。逐次処理より早い。1つreject状態だと全体もreject状態になる。
let jobs = [];
jobs = Promise.all([
asyncProcess('success/data_1'),
asyncProcess('success/data_2'),
asyncProcess('success/data_3'),
]);
// 分割代入でPromiseの配列を個々のの変数に分解して代入する
jobs.then(([response1, response2, response3]) => {
console.log(response1.body);
console.log(response2.body);
console.log(response3.body);
});
// 複数の非同期処理の並列処理 promise.race()
// 複数の非同期処理の内で1番早く成功または失敗したPromiseが返される。レースさせるイメージ。
// 以下のような関数を使って非同期処理のタイムアウト処理を実装できる。
function timeout(timeoutMs) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error(`Timeout: ${timeoutMs}ミリ秒経過`));
}, timeoutMs);
});
}
async function
/* eslint-disable prefer-const */
// 参考ページ https://jsprimer.net/basic/async/
// asyncキーワードで非同期関数を定義でき、これは解決されたpromiseを返す関数である。値はPromiseインスタンスにラップされる。
// ケース1: 値を返すと、その値をもつfulfilled Promiseが返される。
async function doasyncA() {
return 'この値で解決されました。';
// 明示的に解決されたPromiseを返すことを示す記法
// return Promise.resolve('この値で解決されました。');
}
console.log(doasyncA());
doasyncA().then((value) => {
console.log(value);
});
// ケース2: Promiseを返すと、それがそのまま返される
async function doasyncB() {
return Promise.resolve('解決された値');
// catchできない?rejectとcatchの関係がうまくいかない...
// return Promise.reject('エラーメッセージ');
}
console.log(doasyncB());
// ケース3: 例外が発生したら、エラーを持つrejectedなpromiseが返される。
// 非同期関数の表現いろいろ
// 関数宣言
async function funcA() {}
// 関数式
const funcB = async function () {};
// 関数式 arrow関数
const funcC = async () => {};
// // メソッドの短縮記法のAsync Function版 ?
const obj = { async method() {} };
// await式はどんな場合に何を返すか?
// awaitの右辺のPromiseが解決されるとawait式は値を返す。
// then(callback)を書かなくていいことがポイント。
async function doasyncC() {
const value = await Promise.resolve('解決された値');
console.log('このステップは上の非同期処理が完了しないと実行されない。');
console.log(value);
}
doasyncC();
console.log(
`非同期関数doasyncCの完了をawaitしているわけではないのでこのステップは同期的に実行されます。
await式を書いたasync functionの外では、同期的に処理が進む、という挙動をわかっておくこと。`
);
// Promiseはreject状態なのでawaitはその場でエラーをスローする。
// 通常の非同期処理のように次のステップの実行が行われないので、tryブロック内で例外を自動的にキャッチできる。
// then(callback).catch(callback)のように書かなくていいことがポイント。try...catch構文で非同期処理のエラーも扱えるようになる。
async function doasyncD() {
try {
const value = await Promise.reject(new Error('エラーメッセージ'));
} catch (error) {
console.log(error.message);
}
}
doasyncD();
// async/await構文で逐次的な非同期処理を書く。
function dummyFetch(path) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (path.startsWith('success/')) {
resolve({ body: `${path}のデータ` });
} else {
reject(new Error('データ取得失敗しました。'));
}
}, 1000 * Math.random());
});
}
// then(callback)で非同期処理を順次呼び出すようなややこしい記法をしなくてすむことがポイント。
// 同期処理かのように非同期処理の逐次処理を書けるようになっている。
async function fetchResource() {
let results = [];
const responseA = await dummyFetch('success/a'); // データを取得
results.push(responseA.body); // データを配列に格納
const responseB = await dummyFetch('success/b');
results.push(responseB.body);
return results;
}
fetchResource().then((results) => {
console.log(results);
});
/**
* 複数の非同期処理をループ内で順番に実行する。直列、逐次処理。
* async/await構文 と for文 の組み合わせ
* awaitで各処理の実行完了を待つので同期処理のようになり時間がかかる。
*/
/**
* 複数の非同期処理を互いに完了を待たずに並列して実行する。
* async/await構文 と PromiseAll() の組み合わせ
* 実行順序に意味がないなら逐次実行より実行時間が短くなる。
*/
疑問
Promise でのエラーハンドリングがいまいちすっきりしない。
Fetch メソッド
ネットワークリクエストを送信してサーバーから情報を取得するためのメソッド
- リクエストするとレスポンスが Promise として返される。(レスポンスヘッダで resolve する)
- 結果(PromiseResult)を読み取り何らかの形式で受け取る。(レスポンスボディーを読み込む)
構文は async/awit 構文と promise 構文がある。
レスポンスに関する処理の例
- レスポンスの成功失敗を判定する
- レスポンスの HTTP ステータスコードを取得する
- レスポンスのヘッダー情報を取得する
- 本文を JSON として読み取り JavaScript オブジェクトとして取得する
- 本文をテキストデータとして取得する
- 画像を blob として取得して画面に表示する
リクエストに関する処理の例
- POST メソッドでリクエストを作成して送信する
- method の指定
- header の指定
- body の指定
- JSON データをサーバに送信する
- 画像をバイナリデータとしてサーバに送信する
タスク 非同期関数 getUsers の作成
このページのタスクをやってみる。https://ja.javascript.info/fetch
Github の Users APIにリクエストする。 fetch メソッドを実行すると Promise が返され Response オブジェクトとして resolve される。Response オブジェクトの ok プロパティでレスポンスが成功しているかどうかを判定できる。レスポンスが失敗していたら status プロパティでステータスコードを読み取ってアラートする。レスポンスが成功していたら json メソッドで結果を取得する。json メソッドは本文を json として解釈し、JavaScipt オブジェクトに解決される Promise を返す。ブラウザの開発者ツールの Network タブで発生している通信に関する情報を確認できる。
コードを書いてテストできるようになっているサイトがあるので試した。
https://plnkr.co/edit/nqkWxS2MgvfLQOum?p=preview&preview
書いたコードをテストしてみると、とりあえず pass している。
/**
* Githubユーザー情報を取得する非同期関数です。
* @param {string[]} names Githubユーザー名
* @returns {Object[]} Githubユーザー情報
*/
async function getUsers(names) {
let users = [];
try {
for (let name of names) {
let response = await fetch(`https://api.github.com/users/${name}`);
if (response.ok) {
let user = await response.json();
users.push(user);
} else {
users.push(null);
let httpStatusCode = response.status;
console.log(
`HTTP ERROR : ${httpStatusCode} ${name}の取得リクエストは失敗しました。`
);
}
}
return users;
} catch (error) {
alert(error);
}
}解答を読んだ上でコードレビューをする。
リクエストはお互い待つ必要はありません。データはなるべく早く取得できるようにしてください。
ここがポイント。上記コードでは 1 つのリクエストの処理が完了してから次のリクエストを行っている。await をつけているから1つずつ処理している。
リクエストの成功失敗は response.ok で判定したが、解答コードでは then メソッドで処理を分けている。リクエスト成功ならステータスコードが 200 かどうかで分岐させる。
fetch メソッドで Promise が返されたら、そのまま then メソッドに渡して、解決された場合と、拒否された場合の処理を書いている。こうすると、他のリクエストの完了を待たない処理になる。レスポンスがあれば、then のハンドラー関数が非同期に呼び出される。成功していたらすぐに json メソッドで読み取りを行っている。
拒否では null を返す。解決された場合もステータスコードが 200 ではないなら null を返す。200 なら結果の値(JavaScript オブジェクト)を返す。job にはユーザー情報のオブジェクトか null が格納される。
then メソッドのコールバック関数の引数は Response オブジェクトを参照している。then メソッドは Promise を返す。
変数 job は Promise を参照していて、結果のオブジェクトを参照していない。 jobs は Promise の配列である。
jobs を Promise.all()に渡してすべての Promise が解決されるのを await で待ってから結果を取得している。
複数のリクエストを行いデータを読み込む時に並列の非同期処理を行う例。
直列の非同期処理と並列の非同期処理の違い。データをなるべく早く取得するならリクエストは互いに待たない方がいい。
async/await だけでなく Promise 構文も有用。
javascript.info の Fetch チャプターで示されている解答コードみて書き直す。
/**
* Githubユーザー情報を取得する非同期関数です。
* @param {string[]} names Githubユーザー名
* @returns {Object[]} Githubユーザー情報
*/
async function getUsers(names) {
let jobs = [];
for (let name of names) {
let job = fetch(`https://api.github.com/users/${name}`).then(
(successResponse) => {
if (successResponse.status !== 200) {
return null;
} else {
return successResponse.json();
}
},
(failResponse) => {
return null;
}
);
jobs.push(job);
}
let results = await Promise.all(jobs);
return results;
}画像を取得して表示する
https://mizunoshoji.github.io/practice/pages/fetch.html
資格情報を持つリクエストともたない匿名リクエスト
通常あるドメインにリクエストをするとそのドメインに属するクッキーが送信される。しかし JavaScript によるクロスドメインリクエストではクッキーが自動で送信されない。例えば fetch でクッキーを同時に送信するには明示的な設定が施されていなければならない。
リクエストする側。fetch のオプションにcredentialsを含める。
fetch('http://another.com', {
credentials: "include"
});
レスポンスするサーバ。レスポンスヘッダーにAccess-Control-Allow-Credentials: trueを含める。
クッキーで認証を行うサーバが資格情報をもつクロスドメインリクエストを無条件に許可していると、ユーザーになりすます不正リクエストが発生してしまう。
疑問
- fetch メソッドはどのオブジェクトのメンバーなのか?
- Cookie を認証に使う仕組みが細かくわかっていない。実践的に理解はできていない。
async/await 構文
async/await は非同期処理をシンプルに書くことができる構文である。promise.then/catch を使う書き方よりも簡潔だということになっている。
async キーワードを関数の前に置くと Promise が返されることを保証する。値は解決された Promise にラップされる。このように宣言された関数は非同期関数と呼ばれる。
async function f() {
return 1;
}
console.log(f());
コンソールへの出力をみると PromiesResult の値が 1 になっている。
Promise
[[Prototype]]: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: 1
解決された Promise を返すことを明示的に示すこともできる。
async function f() {
return Promise.resolve(1);
}
async キーワードがついた関数内では await キーワードが使用できる。await 式。
let value = await promise;が基本的な構文。Promise の前に await を置くと、Promise が解決されるまで JavaScript を待機させる。Promise が正常に解決されたら結果を返し、失敗したらエラーをスローする。
await 式により promise の非同期処理を同期処理のように扱える。というのも非同期処理が完了するまで JavaScript は待機状態になるから。ということは Promise チェーンの書き方で then メソッドをつないで実現していたような非同期処理の逐次処理を await でも実現できるということ。やることは同じ。可読性の向上に重点がある。
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('done.'), 3000);
});
let result = await promise;
console.log(result);
}
f();
この例では Promise コンストラクターで setTimeout メソッドによる非同期処理を定義している。3 秒間は Promise が解決状態にならない。await によって promise が解決されるまでは JS による処理を待機している。3 秒後に解決されたら result に値がセットされて、done.が出力される。
非同期処理のエラーをキャッチするは try..catch でできる。
async function f() {
try {
let response = await fetch('http://no-such-url');
} catch (err) {
console.log(err);
}
}
メモ
- Promise は実践的に非同期処理を書いてみないと十分に理解できないし書き方が身につかない。
- そもそも何で Promise の解決を待つための await が必要なのか? –> 結果を後の処理で使う場合に次の処理が実行されてしまうと困る。確実に結果を得てから後の処理をしたい状況がある。
- フロントエンド実装で必要になる非同期処理とはどういうもの?
- WebAPI にリクエストしてデータを取得する
- 画像の読み込み
タスク
- 非同期処理の基本的なパターンを叩き込む –> ひととおり基本は目を通して軽く触れたので後は実践で覚えていく。
- fetch で画像を読み込んで表示する
- ローカルの画像
- 外部から取得した情報
- おもしろそうな Web API を調べる
- d3.js と組み合わせてデータを視覚化してみるのにいい API あるか?
- WebAPI を作るには?
- WebAPI についてきちんと理解できる本を買う
- Ajax つかったアプリのアイデアを出す