JavaScriptの非同期処理について「そういうことか〜!!」と思ったことを含む備忘録です
Fetch API(HTTPリクエストを送るインターフェース)など多くは非同期で実装されています(最近のものはほぼ「Promise」がベースです)
「自分で実装する」する機会は少なく、非同期APIの正しい使い方
要するに、処理(主に時間がかかる処理)の完了後に「どのような処理をするか」を適切に登録する(ハンドラ)ことが重要です
*特に処理(主に時間がかかる処理)の結果を利用する場合がポイントです
非同期処理について
「プログラミング言語」でいう「非同期処理」はひとつの処理が終わるのを待たずに次の処理を評価します(同時に複数の処理)
*ちなみに「同期処理」はひとつの処理が終わるまで次の処理は行いません(コードを上から順番に処理します)
JavaScriptの場合は
「Node.js」は「シングルスレッド」で「ブラウザのJavaScript」は「ほぼメインスレッド」で実行されます
同時に複数の処理は行いません(マルチスレッドで別スレッドを立ちあげたり、ウェブワーカーという手段を使ったりは通常しません)
例えば、時間のかかる処理が完了する間ブラウザのメインスレッドが何も処理できないと大変です!!
JavaScriptは通常、同時に複数の処理を行いませんが、その代わりに処理(主に時間のかかる処理)が完了するまではスレッドから切り離し、待ち時間で他の処理をする手法(イベントループ)があります
JavaScriptの非同期関数はこの仕組みで実装されています
//同期
console.log('1')
console.log('2')
console.log('3')
//1
//2
//3
//非同期
console.log('1')
//1秒待っている間に次の処理が先に実行される
setTimeout(() => {
console.log('2')
}, 1000);
console.log('3')
//1
//3
//2
コールバック関数
コールバック関数そのものについて
コールバック関数は関数の引数として渡される関数のことで、関数内で実行されます
*下記のコードはアロー関数ですが関数宣言や無名関数の場合もあります
関数が実行されたタイミングでコールバック関数が実行されます(関数→コールバック関数)
コールバック関数に引数(仮引数)がある場合の実引数は関数内にあります(関数内でコールバック関数が実行されて渡ってきます)
function tellName(func) {
//コールバック関数を実行 実引数あり
func('私の名前は');
}
//argは仮引数
tellName(arg => console.log(arg + '花子'));
// 私の名前は花子
コールバックで複数の処理を順番に実行したい場合は、ネストが深くなります(コールバック地獄)
複数の処理を順番に実行したい場合とは、処理結果を次の処理で利用する場合などです
下記コードではstep1の結果を使ってstep2を呼び出してその結果を使ってstep3を呼び出しています
function step1(init, func) {
const result = init + 1;
func(result);
}
function step2(init, func) {
const result = init + 2;
func(result);
}
function step3(init,func) {
const result = init + 3;
func(result);
}
function hoge() {
step1(0, (result1) => {
step2(result1, (result2) => {
step3(result2, (result3) => {
console.log(result3);
});
});
});
}
hoge();
// 6
深いネストはコードが読みにくく、エラー処理が難しくなります
そのため「Promise」登場後のほとんどの「非同期API」では「コールバック」ではなく「Promise」をベースにしています
コールバック関数(非同期)
setTimeout(処理内容,指定時間)
は指定時間経過後(完了後)の処理をコールバック関数で登録するタイプの代表的な非同期関数です
指定時間経過後(指定時間経過というタスク完了後)に処理内容(コールバック関数)を実行します
setTimeout(() => {
console.log('done')
}, 1000);
特定の非同期関数の後に関数を実行したいとき
「例1」では非同期関数はsetTimeoutの指定時間がない場合でも「b」が先に実行されます
*a()→b()→setTimeoutのハンドラの順番(イベントループの仕様により)
「例2」では関数aの引数に後で実行したい関数を渡してsetTimeoutの中で実行しています
これで「b」がsetTimeout(非同期関数)より後に実行されます(例えば、非同期関数の結果を利用できるようになります)
//例1
function a() {
setTimeout(()=>console.log('a'))
}
function b() {
console.log('b');
}
a()
b()
//b
//a
//例2
function a(callback) {
setTimeout(()=>{
console.log('a')
callback()
})
}
function b() {
console.log('b');
}
a(b)
//a
//b
Promise
Promiseは非同期処理の状態や結果を表現するオブジェクトです
Promiseを使った非同期関数はsetTimeoutのようにコールバック関数を引数に取るのではなく
先にPromiseオブジェクト返してから時間が掛かる処理を実行します
詳細はあとにして、
あえてsetTimeoutをPromise化(Promiseオブジェクトを返す関数を作成)してみます
Promiseのコンストラクタはresolve関数とreject関数を引数にとる関数です
*PromiseチェーンにするにはthenメソッドでPromiseインスタンスのreturnが必要です
function sleep(time){
return new Promise((resolve, reject) => {
setTimeout(()=>{
resolve(time)
}, time)
})
}
sleep(500)
.then(res=>{
console.log(res)
return sleep(1000)
})
.then(res=>{
console.log(res)
return sleep(1500)
})
resolve()
:resolveを実行すると成功ですreject()
:resolveを実行すると失敗です
最初Promiseオブジェクトの状態は「pendeing」(保留中)です
成功した場合は状態は「fulfilled」になり、「thenメソッド」に書かれた処理が実行されます
失敗した場合は状態が「rejected」になり、「catchメソッド」に書かれた処理が実行されます
- pending:まだ非同期処理は終わっていない(保留中)
- fulfilled:非同期処理が成功
- rejected:非同期処理が失敗
Promiseオブジェクトのメソッド(thenとcatchとfinally)
*処理(時間がかかる処理)が完了したときまたは失敗したときに「どのような処理をするか」を登録する
then()
:2つのコールバック関数を登録できます
1つ目は成功したときに実行する関数
2つ目は失敗したときに実行する関数です(catch()
でも書けます)catch()
:失敗したときのPromiseオブジェクトを受け取ります(エラー処理専用)finally()
:成功か失敗かにかかわらず実行されます
引数は取りません(Promiseチェーンの最後に必ず呼び出したい処理に使います)
thenメソッドのコールバック関数の引数
resolve(値)
の引数の値(通常時間がかかる処理結果の値)はthenに登録するコールバック関数の引数に渡ります
new Promise(resolve => resolve("Promise"))
.then(arg => console.log(arg))
//Promise
const myPromise= new Promise((resolve) => {
//成功
resolve("成功!")
}).then((result) => {
console.log(result);
});
//成功!
const myPromise= new Promise((resolve,rejected) => {
//失敗
rejected(new Error("失敗!"))
}).catch((result) => {
console.log(result);
});
//Error: 失敗!
const myPromise= new Promise((resolve) => {
resolve()
//thenメソッドでエラーを投げることもできます
}).then(() => {
throw new Error("エラー");
}).catch((result) => {
console.log(result);
});
//Error: エラー
Promiseチェーン
*fetch()
は引数のURLにリクエストをしてPromiseオブジェクトを返す非同期関数です
thenメソッドのコールバック関数の戻り値もPromiseオブジェクトです
そのPromiseオブジェクトを使ってthenメソッドを繋ぐことができます
順番に実行(直列)します
*Promiseチェーンを繋ぐには必ずPromiseオブジェクトを返します
*Promiseチェーンのどこかでエラーになった場合は、そこからcatchメソッドで処理されます(エラー処理が簡単です)
const fetchPromise = fetch('https://jsonplaceholder.typicode.com/todos/1');
fetchPromise
.then(response => response.json())
//.then(response => {
// return response.json()
// })
.then(json => console.log(json))
Promise.all(反復可能オブジェクト、例えば Array)
並列処理:Promise.allのthenメソッドには複数の非同期関数のすべてが完了したときの処理を登録します
1つでも「reject() (失敗)」の場合は処理を打ち切ります
Promise.all([
new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3
]).then((val)=> console.log(val));
// [1, 2, 3]
Promise.race(反復可能オブジェクト、例えば Array)
Promise.raceのthenメソッドは、複数の非同期関数のどれか1つが完了したときの処理を登録します
Promise.race([
new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3
]).then((val)=> console.log(val));
// 3
async await
関数の先頭に「async」を追加すると非同期関数になります(関数はPromiseを返します)
「async」がついている関数ブロック内でのみ「awaitキーワード」が使用できます
「awaitキーワード」はPromiseオブジェクトを返す処理の前に付けます
「resolve()
(成功)」を待ってから次に進み、「await」は順番に処理されます
「resolve(値)
」の値は awaitを付けた関数の戻り値です
*値(結果)はthenメソッドのようにコールバック関数の引数に渡るわけではありません
async関数内では同期処理と同じように処理されます(例1)
*(例2)ではasync関数ブロックの外側でconsoleに出力しています
この場合は、Promiseオブジェクトのthenメソッドのコールバック関数にする必要があります
//例1
async function load() {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const data = await response.json()
console.log(data)
}
load()
//例2asyncの外側でconsoleに出力する場合
async function load() {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const data = await response.json()
return data
}
const promise = load();
promise.then((data) => console.log(data));
「reject()
(失敗)」は「 try…catch 文」でキャッチします
async function load() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
if (!response.ok) {
throw new Error('エラー');
}
const data = await response.json();
console.log(data);
}
catch (error) {
console.log(error);
}
}
load()
例えば(Node.js File system)
すべてのFile system操作には、Promiseベース・コールバック・同期があります
ファイルの読み取りの例
Promiseベースの操作
非同期操作が完了するとpromiseを返します
import fs from 'fs/promises';
(async function() {
const data = await fs.readFile('sample.txt', 'utf8');
console.log(data);
})();
コールバック(第一引数は例外用、第二引数は完了した時)
import fs from 'fs';
fs.readFile('sample.txt', 'utf8', function(err, data) {
if (err) throw err;
console.log(data);
});
同期(操作が完了するまでJavaScriptの実行をブロックします)
import fs from 'fs';
let data = null;
try {
data = fs.readFileSync('sample.txt', 'utf8');
console.log(data);
} catch(e) {
//エラー処理
}