【メモ】JSの非同期処理について学んだことまとめ
62902 ワード
下記講座で学んだ非同期通信について、整理してみる
使用しているAPI:
-
https://restcountries.com/
国情報を取得できるAPI -
https://geocode.xyz/
緯度経度からその土地の情報を取得できるAPI
前提知識
- imgのloadやajax、timerは非同期であり、JSのcall stackではなくwebAPI environmentにて処理される
非同期処理のコールバック
- web API environmentに登録される → 発火したらcallback queueへ移動、queue内の処理が実行される
- call stackにglobal context以外実行中の処理が無ければ(さらにmicrotasks queueが無ければ)、callback queueの1つ目がcall stackに渡される(event loop)
promise
- web API environmentに登録される → 発火したらmicrotasks queueへ移動(callback queueよりも優先される)
- microtasks queueの処理が新たなmicrotasks(thenのchainなど)を作り出しても、callback queueより優先される
XMLHttpRequest
古い書き方であり、今はfetchを使う方法が主流
const request = new XMLHttpRequest();
request.open('GET', `https://restcountries.com/v3.1/name/mexico`);
// data = undefined(非同期でbackgroundで処理中のため、データは入らない)
// const data = request.send();
request.send();
console.log(request.responseText); // まだ何も返っていない
// データが返ってきたら発火
request.addEventListener('load', function () {
// responseをjson形式にする
const [data] = JSON.parse(this.responseText);
console.log(data);
// 取得したデータから隣国名を取得
const [neighbour] = data.borders;
// 上記で取得した隣国のデータを取得するには、今いるコールバック関数内で新しくrequestを投げることになる
const request2 = new XMLHttpRequest();
request2.open(
'GET',
`https://restcountries.com/v3.1/alpha/${neighbour}`
);
request2.send();
// requestした隣国データの取得は新しいコールバック内にて取得、となり、所謂callback hellに陥る
request2.addEventListener('load', function () {
const data2 = JSON.parse(this.responseText);
console.log(data2);
});
});
fetchとpromise
- fetchはpromiseを返す
- promiseは値を受け取るプレースホルダーであり、fulfilled or rejectedいずれかの状態になる
- fulfilledはthenの第1引数に入るコールバックの引数に入り、rejectedはthenの第2引数に入るコールバックの引数、もしくはcatchの第1引数に入るコールバックの引数で受け取れる
- then/catchは常にpromiseをreturnし、戻り値はそのpromiseのfulfilled valueになる(次のthenのコールバックの引数で受け取れる)
- thenでthrowするとrejectedとなり、throwした値をrejected valueとして受け取れる
- fetchがrejectedを返すのは、no internet connection時のみ(responseのステータスコードが200でなくても)なので、response.ok(200ならtrue)などで明示的にthrowする
// fetchがpending promiseを返し、成功(fulfilled)すればthenのコールバックの引数でresponseを受け取れる
// no internet connectionでない限り、fetchはfulfilled valueを返す
fetch(`https://restcountries.com/v3.1/name/mexico`)
.then((response) => {
// 国が見つからなくてもfetchはfulfilledを返すので、手動でerrorを投げる
// errorをthrowするとpromiseはrejectedを投げる(= catchで受け取れる)
if (!response.ok) {
throw new Error(`Country not found (${response.status})`);
}
// 受け取ったresponseはjson形式にする必要があるが、response.json()も非同期であるため、thenで受け取る必要がある
return response.json();
})
// ここでjson形式のdataになる
.then((data) => {
console.log(data[0]);
const [neighbour] = data[0].borders;
// fetchの戻り値(promiseのfulfilled value)をreturnすることで、次のthenでfulfilled valueとして受け取る
// ここでfetch().then()のようにするとcallback hellになるので、しないこと
return fetch(`https://restcountries.com/v3.1/alpha/${neighbour}`);
})
// neighbourのpromise処理
.then((response) => {
if (!response.ok) {
throw new Error(`Country not found (${response.status})`);
}
return response.json();
})
.then((data) => console.log(data[0]))
// いずれのpromiseのrejectedもcatchで受け取れ、コールバックの引数でrejected valueを受け取れる
.catch((err) => {
console.error(`${err} 💣💣💣`);
})
// fulfilled, rejectedに関係なく最後に呼ばれるので、共通処理に便利
.finally(() => console.log('done'));
callback queueとmicrotask queue
- microtask queueはcallback queueより優先される
- promiseはmicrotask queueになる
// logが表示される順序は、test start → test end → promise1 → promise2 → setTimeout
// promiseが実行されている間はcallback queueは実行されないため、
// 下の例ではpromise2の処理に時間がかかり、完了後setTimeoutが呼ばれるため、実際には0秒で実行されない
console.log('test start');
setTimeout(() => console.log('0 sec timer'), 0);
Promise.resolve('Resolved promise 1').then((res) => console.log(res));
Promise.resolve('Resolved promise 2').then((res) => {
for (let i = 0; i < 300000000; i++) {}
console.log(res);
});
console.log('test end');
Promiseオブジェクトの基本
- new Promiseはpromiseを返す
- resolveを返すとfulfilledになり、rejectを返すとrejectedになる
- resolve / rejectの引数が、それぞれfulfilled value / rejected valueになる
const lotteryPromise = new Promise(function (resolve, reject) {
console.log('lottery draw is happening');
setTimeout(() => {
Math.random() >= 0.5
? resolve('You WIN')
: reject(new Error('You lost your money'));
}, 2000);
});
// pending promiseが入り、2秒後にresolve or rejectedが来る
lotteryPromise
.then((res) => console.log(res))
.catch((err) => console.error(err.message));
即時でresolve / rejectを返す方法
Promise.resolve('success!').then((x) => console.log(x));
Promise.reject(new Error('problem!')).catch((x) => console.error(x));
promiseのresolve / rejectを他のfunctionと組み合わせる(geolocation APIの例)
// 下記をpromiseで書き換える(promisify)
// getCurrentPositionは第1引数が位置情報取得成功時に実行されるコールバック、第2引数は失敗時に実行されるコールバック(非同期処理)
navigator.geolocation.getCurrentPosition(
(position) => console.log(position),
(err) => console.error(err)
);
new Promiseを返すようにすれば、getPosition().then()...のように繋げられる
const getPosition = function () {
return new Promise(function (resolve, reject) {
navigator.geolocation.getCurrentPosition(
// 成功時はresolveでthenに繋げ、失敗時はrejectでcatchに繋げる
(position) => resolve(position),
(err) => reject(err)
);
});
};
getPosition().then((pos) => console.log(pos));
上記をさらにシンプルに書く
const getPosition = function () {
return new Promise(function (resolve, reject) {
navigator.geolocation.getCurrentPosition(resolve, reject);
});
};
getPosition().then((pos) => console.log(pos));
async / await
- async functionは全てバックグラウンドで処理し、戻り値はpromiseになる
- async内の非同期処理(promiseがreturnされる部分)でawaitを使用すると、fulfilled valueが入るまで処理を止められる(execution blockが発生し、順番に実行される)
- main thread内ではないので、call stackはblockされない
- then/catchの様な機能は無いため、try/catchでエラーを拾う
- async内のtry/catchは、returnを返せば(もしくは何も返さなければ)fulfilled, throwすればrejectedの挙動になる
async function whereAmI() {
try {
// awaitしているので、fulfilled valueが入ってから次へ行く
// 失敗した場合はrejectedとなり、catchへ行く
// await書かないと処理が進んでposにはpromiseが入るので、pos.coordsがerrorになる
const pos = await new Promise((resolve, reject) =>
navigator.geolocation.getCurrentPosition(
resolve,
reject
)
);
const { latitude: lat, longitude: long } = pos.coords;
console.log(lat, long);
// reverse geocoording
const resGeo = await fetch(
`https://geocode.xyz/${lat},${long}?geoit=json`
);
if (!resGeo.ok)
throw new Error(`Problem getting locaiton data`);
const dataGeo = await resGeo.json();
console.log(dataGeo);
// 国情報を取得
const res = await fetch(
`https://restcountries.com/v3.1/name/${dataGeo.country}`
);
if (!res.ok)
throw new Error(`Problem getting country data`);
const data = await res.json();
console.log(data);
// promiseをreturnする
return `You are in ${dataGeo.city}, ${dataGeo.country}`;
} catch (err) {
console.error(`${err}💣💣💣`);
// catchしたerrorを(returnでは無く)throwしない限り、このwhereAmIが返すpromiseはfulfilledになる
throw err;
}
}
async functionの戻り値はpromiseなので、そのままではpromiseが入ってしまう
const city = whereAmI();
console.log(whereAmI(city)); // Promise {<pending>}
方法①:then/catchで受け取る
whereAmI()
.then((res) => console.log(res))
.catch((err) => console.error(err));
方法②:さらにasync functionで囲み、awaitで受け取る
- thenと混在させずに済む(callbackも増やさずに済む)
- funcitonを増やしたく無ければ、下記のように即時実行関数を使用するのもあり
(async function () {
try {
// whereAmIがreturnならfulfilledが、throwならrejectedが返ってくる
const city = await whereAmI();
console.log(`fulfilled:${city}`);
} catch (err) {
console.error(`rejected:${err}`);
} finally {
// try catchの結果に関わらず実行される
console.log('finished');
}
})();
Promise.all()
- promiseを同時並行で実行する
- promiseをarrayで受け取り、新しい1つのpromiseを返す
- 全てfulfilledなら、それぞれのfulfilled valueのarrayをreturnする
- 引数の内1つでもrejectされたら、新しいpromiseもrejectされる
(async function () {
// 1つ終わったら次、となるので、時間がかかる
// const res1 = await fetch(`https://restcountries.com/v3.1/name/italy`);
// const res2 = await fetch(`https://restcountries.com/v3.1/name/egypt`);
// const res3 = await fetch(`https://restcountries.com/v3.1/name/mexico`);
const res = await Promise.all([
fetch(`https://restcountries.com/v3.1/name/italy`),
fetch(`https://restcountries.com/v3.1/name/egypt`),
fetch(`https://restcountries.com/v3.1/name/mexico`),
]);
})();
Promise.race()
- promiseを引数に取り、最初に処理が返ったpromiseを取得
- そのpromiseのfulfilled value or rejected errorが全体のPromise.race()の状態として機能する
(async function () {
const res = await Promise.race([
fetch(`https://restcountries.com/v3.1/name/italy`),
fetch(`https://restcountries.com/v3.1/name/egypt`),
fetch(`https://restcountries.com/v3.1/name/mexico`),
]);
})();
Promise.allSettled()
状態に関わらず全てのpromiseを返す(allの方は一つでもrejectされたら中断)
(async function () {
const res = await Promise.allSettled([
Promise.resolve('success'),
Promise.reject('error'),
Promise.resolve('success2'),
]);
})();
Promise.any()
fulfilledとなる最初のpromiseを取得(rejectは無視)
(async function () {
const res = await Promise.any([
Promise.reject('error'),
Promise.resolve('success'),
Promise.resolve('success2'),
]);
})();
asyncのreturnの気をつけるべき挙動
asyncはpromiseをreturnする
loadAll(['img/img-1.jpg', 'img/img-2.jpg', 'img/img-3.jpg']);
async function loadAll(imgArr) {
try {
/**
* 前提:
* ・mapのコールバックではcreateImageをawaitしているので、createImageのfulfilled valueであるimg Elementがコールバックの戻り値になりそうだが、
* コールバック自体がasync functionかつawaitされていないので、実際にはimgsにはpromiseが返る
* ・またそもそもコールバック自体がawaitされていないということは、コールバックの実行を待たずloadAll内の処理が進むということ
*
* 流れ:
* 1.上記の通りimgsには(createImageでは無く)async コールバックのpromiseが入り、下記のPromise.all()まで先に処理が進む
* 2.Promise.all()がawaitしているので、imgs(mapのそれぞれのコールバックのpromise)がそれぞれreturn valueを返すまで待つ
* 3.それぞれのコールバックは、さらにcreateImageのreturnをawaitしており、img Elementがreturnされると、それをfulfilled valueとしてPromise.all()にreturnする
*/
const imgs = imgArr.map(async (img) => await createImage(img));
// 実際は、上記コールバックでasync/awaitする意味は無いので、こうなる
const imgs = imgArr.map((img) => createImage(img));
console.log(imgs);
/**
* 1.Promise.allがimgsにpromiseを返す
* 2.引数の3つのpromise(mapのコールバック)全てがresolveを返したら、Promise.allもresolveを返す
* 3.それぞれのfilfilled value(imgタグ)のarrayをreturnしimgsElにセットされる
*/
const imgsEl = await Promise.all(imgs);
imgsEl.forEach((img) => img.classList.add('parallel'));
} catch (err) {
console.error(err);
}
}
function createImage(imgPath) {
return new Promise(function (resolve, reject) {
const img = document.createElement('img');
img.src = imgPath;
img.addEventListener('load', () => {
imgContainer.append(img);
resolve(img);
});
img.addEventListener('error', () =>
reject(new Error('Image not found'))
);
});
}
Author And Source
この問題について(【メモ】JSの非同期処理について学んだことまとめ), 我々は、より多くの情報をここで見つけました https://zenn.dev/syamozipc/articles/js_asynchronous著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Collection and Share based on the CC protocol