🔫

PythonからJavascriptの非同期を学んだ時のまとめ。

2020/11/17に公開

最初に

Javascriptの非同期をやっていく上でこれがすごい大事な気がする。

JSは基本的に非同期処理を元に作成された言語

そのためこれから使うcallback関数、Promis、await asyncでは関数を非同期に変換するというより、非同期に待機処理を追加して処理を上手くコントロールするという使い方になる。

他の言語ではこれが逆になるから、少し混乱するのかもしれない。

コールバック関数を使った非同期

setTimeout を使用する事で処理が一度中断して、その後時間が来たら実行される。それまでは別の処理が行われるので非同期処理になったと言える。

Javascriptでのcallback 非同期

簡単な例:関数を定義してコールバックに渡す。

console.log("setTimeoutの前:" + new Date());
function f() {
	console.log("これは関数のfの中:" + new Date());
}

setTimeout(f, 10*1000); // 10秒後にfを実行。コールバック
console.log("setTimeoutの後");
console.log("これもsetTimeoutの後");

/*
setTimeoutの前:Wed Apr 14 2021 09:35:39 GMT+0900 (日本標準時)
VM65:7 setTimeoutの後
VM65:8 これもsetTimeoutの後
undefined
//10秒後に表示される。
VM65:3 これは関数のfの中:Wed Apr 14 2021 09:35:49 GMT+0900 (日本標準時)
*/

無名関数で定義する

console.log("setTimeoutの前:" + new Date());
setTimeout(
	function() {
		console.log("setTimeoutに指定された無名関数の中:" + new Date());
	}, // ここまでsetTimeoutの第一引数(無名関数)
	10*1000
);
console.log("setTimeoutの後");
console.log("これもsetTimeoutの後");
//実行結果は出力される文字列は違うが同じ

アロー関数で定義する。

console.log("setTimeoutの前:" + new Date());
setTimeout( () => console.log("アロー関数の中:" + new Date()), 10*1000);
console.log("setTimeoutの後");
console.log("これもsetTimeoutの後");

//実行結果は出力される文字列は違うが同じ

setIntervalとclearInterval

分が切り替わるまでに繰り返しで実行するので最初に実行する時間次第で実行出来る回数が変わる。

例:18:00:00ならたくさん実行出来るが、18:00:50だとちょっとしか実行出来ない。

const start = new Date();
let i=0;
const intervalId = setInterval(function() {
	let now = new Date();
	if(now.getMinutes() !== start.getMinutes() || ++i>10)
		return clearInterval(intervalId);//ここで10回以上・分が切り替わるとclearIntervalでプログラムが止まる。
	console.log(`${i}: ${now}`);
}, 5*1000);

PythonとJavaScript でのコールバック比較

再起処理でまとめて、一回の処理みたいに扱う事で処理が同期的に行われるように工夫している。

function adding(callback, num){
	console.log(num);//最初は0
	callback(num);//wait(num=>{n++; ....},num};ここでnumをインクリメント
}

//callback()と引数を忘れるとNan(Not a number)が表示される。

//waitのコールバック関数の中でさらにwaitを呼んでそれを繰り返す。
adding(num => {
	num++;
	adding(num => {
		num++;
		adding(num => {
				num++;

		}, num);
	}, num);
}, 0);

//実行結果
0
1
2

Pythonでのcallback 同期

num = 0

def say(callback, num):
	print(num)
	return callback(num)

#無名関数を使用してないので関数が2つになる。
def adding(num):
	num += 1
	return num

num = say(adding, num)
num = say(adding, num)
num = say(adding, num)

#実行結果
0
1
2

Pythonの場合は say で呼ばれた addingreturn が実行されるまで次の処理にはいかないが、JSの場合は待たずに次の処理に行ってしまう。上記のコード例ではあまり違いを提示出来てなくて申し訳ないが、言語として根本的に仕様が違う。JSは非同期でPythonは同期処理そのためJSコードをPythonに変換してコードを記述する際は時折処理を待機させる必要が出てくる。

そのためJSでfetch投げたりしても、レスポンス待たずに次の処理に移行する。

例 ※実際には動作しない。

//サーバ取得
const res = getDataFromServer();//レスポンス待たずに次の処理へ

//取得したデータ加工
res.doDomething();//ここでエラーが発生する。

//全く関係ない他の処理
doSomethingElse();

コールバック関数を用いた非同期を実行する。(setTimeoutを使用して)

function wait(callback, num){
	//setTimeoutではアロー関数(無名関数)で引数がない別の関数を
	//コールバックとして取る。その中でwaitがcallbackとして受け取った無名関数を処理する。
	//0.1秒後に実行される。
	setTimeout(() => {
		//1秒後に出力される。
		console.log(num);
		callback();
	}, 1000);
	console.log(num);//先に出力される。
}

wait(() => {
	//無名関数をwaitのcallbackにする。
	console.log('callback function is called');
//0をnumの引数として取る。
}, 0);

//実行結果
0
0
callback function is called

コールバックチェーンにしてみる。これは地獄らしい。別名:コールバック地獄

こちらは setTimeout で非同期処理になっているが、実行の仕方が再帰処理のため同期的な処理になっている。そして、再帰処理で行わなければ処理は上手く行かなくなる。

コールバック地獄1

function wait(callback, num){
	setTimeout(() => {
		console.log(num);//最初は0
		callback(num);//wait(num=>{n++; ....},num};ここでnumをインクリメント
	}, 100);
}

//callback()と引数を忘れるとNan(Not a number)が表示される。

//waitのコールバック関数の中でさらにwaitを呼んでそれを繰り返す。
wait(num => {
	num++;
	wait(num => {
		num++;
		wait(num => {
				num++;

		}, num);
	}, num);
}, 0);

//最初は一番外側に書かれた処理が行われるので最初の値は0になる。

一行にしてみた。外側から内側に処理が向かっていく感じ

wait(num=>{num++;wait(num=>{num++; wait(num=>{num++;},num); },num);},0);//num 1
wait(num=>{num++; wait(num=>{num++;},num);} ,1);//num 2
wait(num=>{num++;} ,2);//num 3

コールバック地獄2

  1. 3個のファイルを読み込み
  2. そのファイル内容を合体させて4つ目のファイルに書き出す

fsの使い方

fs.readFile(ファイルパス, 文字コード, コールバック関数)
fs.writeFile(ファイルパス, ファイルの中身, コールバック関数)
/*Nodeで実行*/
const fs = require('fs');
fs.readFile('a.txt', function(err, dataA) {
	if(err) console.error(err);
	fs.readFile('b.txt', function(err, dataB) {
		if(err) console.error(err);
		fs.readFile('c.txt', function(err, dataC) {
			if(err) console.error(err):
			fs.writeFile('d.txt', dataA+dataB+dataC, function(err) {
				if(err) console.error(err);
			});
		});
	});
});
		

上記のコードを例外スローしようとすると大変です。

実行結果はエラーを起こし、例外処理をしているように見えるが例外処理は行われずにエラーになっている。

これは try...catch はブロックが同じ関数でしか機能しないというのが原因です。

エラーが出力されるのは fs.readFile のコールバック関数内で try...catch ブロックとは別の所にある。これを解決するために後述されるプロミスが登場する。

/*Node 実行*/
const fs = require('fs);
function readSketchyFile() { //関数名:怪しいファイルを読み込む意味
	try {
		fs.readFile('does_not_exist.txt', function(err, data) {
			if(err) throw err;
			else console.log('無事に読み込めました')
		});
	} catch(err) {
		console.log('警告:マイナーな問題発生。実行を継続します。');
	}
}

readSketchyFile()

スコープと非同期の実行

下記のコードは一見カウントダウン5, 4, 3, 2, 1, Go と出力されそうですが、実際には-1が6回出力される。forループは最後まで実行され、iの値は-1になる。そして、コールバックが実行されるのはその後になる。コールバックは実行時に i は既に -1 になっている。

ここでスコープと非同期の実行がどのように関連しているか理解する。

  1. countdownを起動する時、変数 i を含むクロージャを生成する。
  2. forループ内で生成する無名関数のコールバックの全ては同じ i にアクセスする。

問題はforループの中で i が2つの方法で使われている。タイムアウトの時間を計算するのにのに i を使う際は想定通りに動作します。 ((5-i)*1000) は最初は0、2度目に1秒(1000)3度目に2秒(2000)と行った感じになる。この計算は同期的に行われる。setTimeoutの呼び出しも同期的に行われる。非同期に行われるのはsetTimeoutに渡された無名関数でそこで問題が起きている。

function countdown() {
	let i; //iをループの外で定義している。
	console.log("カウントダウン:");
	for(i=5; i>=0; i--) {
		setTimeout(function() {
			console.log(i===0 ? "GO!" : i);
		}, (5-i)*1000);
	}
}
countdown();

上記の問題が起きる理由はブロックスコープの外側にある変数 i にアクセスする事で起こる。setTimeoutの処理が先に終わり、その後時間が経って処理が行われる際にアクセスする変数が i になっていてその時には 中身が -1 になっているので予期しない動作に繋がる。 解決するにはforループの外側にある let i; の定義をforループ内に定義する。

function countdown() {
	console.log("カウントダウン:");
	for(let i=5; i>=0; i--) {
		setTimeout(function() {
			console.log(i===0 ? "GO!" : i);
		}, (5-i)*1000);
	}
}
countdown();

IIFEで解決する場合

参考にした。

Explaining IIFE Inside Loop

function countdown() {
	console.log("カウントダウン:");
  let i = 5;
  for(i=5; i>=0; i--) {
    (function(i){
      setTimeout(function() {
        console.log(i===0 ? "GO!" : i);
      }, (5-i)*1000);
    })(i)
  }
}
countdown()

プロミスとは

プロミスはコールバックを不要にしてくれるものではない。プロミスによってコールバックが定型的なパターンで処理され、コールバックだけだと見つかりにくいバグや分かりにくい記述をなくしてくれる。

プロミスのアイデアは非同期な処理をする関数を呼び出すとオブジェクトPromiseのインスタンスが返される。このとき返されるプロミスは非同期な処理をラップしている。そのプロミスは完了(fulfilled 成功)されるか破棄(rejected 失敗)されるのいずれかが起きる事が保証される。完了も破棄もされていない状態を保留(pending)という。

プロミスは非同期処理の内容を記述した関数を引数に指定してPromiseのインスタンスを生成する。

プロミスの生成

new Promise(非同期処理を記述した関数);
//その引数にとった関数の引数は2つ必要とする。

//実際のコードにすると
new Promise(
	function(onFulfilled, onRejected) {
		.../*非同期処理を記述*/
	}
)

//onFulifilledは処理が正常に終了して結果が得られた場合に実行させる。
//onRejectedは処理の結果エラーが起きた場合に実行させるものプロミスが失敗した。

カウントダウンのプログラムをプロミスで書き換える

function countdown(seconds) {
	return new Promise(
		function(onFulfilled, onRejected) {
			for(let i=seconds; i>=0; i--) {
				setTimeout(function() {
					if(i>0) console.log(i + '...');
					else onFulfilled(console.log("GO!"));
				}, (seconds-i)*1000);
			}
		}
	);
}
countdown(5)

上記のままではプロミスのメリットがないので、カウントダウンが成功した際の処理を追加していく。

function countdown(seconds) {
	return new Promise(
		function(onFulfilled, onRejected) {
			for(let i=seconds; i>=0; i--) {
				setTimeout(function() {
					if(i>0) console.log(i + '...');
					else onFulfilled(console.log("GO!"));
				}, (seconds-i)*1000);
			}
		}
	);
}

countdown(5).then(
	function() { /*成功(fulfilled)時に行う処理を記述する。*/
		console.log("カウントダウン成功");
	},
	function(err) {/*失敗(rejected)時に行う処理を記述する。*/
		console.log("カウントダウンでエラーが起こった:" + err.message);
	}
);

//実行結果
//5...
//4...
//3...
//2...
//1...
//GO!
//カウントダウン成功

上記の例では戻されたプロミスを変数には代入せずにメソッドthenを直接呼び出している。(このメソッドのことを「thenハンドラ」と呼ぶ事がある。)

上で書いたようなプロミスはfulfilledあるいはrejectedのいずれかで、関数が両方呼び出されることはなく、呼び出されるとしてもいずれか一方だけになる。

プロミスにはメソッドcatchがありこれを使うとハンドラの処理を成功と失敗の場合の2つに分ける事ができる。下記の例ではプロミスを変数に一旦保存する。

function countdown(seconds) {
	return new Promise(
		function(onFulfilled, onRejected) {
			for(let i=seconds; i>=0; i--) {
				setTimeout(function() {
					if(i===13) return onRejected(new Error("この数は不吉過ぎます"));
					if(i>0) console.log(i + '...');
					else onFulfilled(console.log("GO!"));
				}, (seconds-i)*1000);
			}
		}
	);
}

const p = countdown(15);
p.then(function() {
	console.log("カウントダウン成功")
});
p.catch(function(err) {
	console.log("カウントダウンでエラーが起こった:" + err.message);
});

//実行結果
15...
14...
カウントダウンでエラーが起こった:この数は不吉過ぎます
Uncaught (in promise) Error: この数は不吉過ぎます
    at <anonymous>:6:35
(anonymous) @ VM52:6
Promise.then (async)
(anonymous) @ VM52:16
12...
11...
10...
9...
8...
7...
6...
5...
4...
3...
2...
1...
GO!

13になった時に失敗しますが、関数自体は止まりません。 onRejected, onFulfilled を呼んだだけでは関数は止まらない。プロミスは状態を管理するだけで、内部の処理については感知しない。既にsetTimeoutで処理の実行が予約されているのでカウントダウンは続く。

なので関数が成功あるいは失敗したら停止するようにプログラムを変更する。

処理を止める必要になった時に保留中のsetTimeoutを全てクリアする。例えばsetTimeoutから返されるIDを全て覚えておいて、clearTimeoutを呼び出す。

function countdown(seconds) {
	return new Promise(function(onFulfilled, onRejected) {
		const timeoutIds =[];
		for(let i=seconds; i>=0; i--) {
			timeoutIds.push(setTimeout(
				function() {
					if(i===13) {
						timeoutIds.forEach(clearTimeout);
						onRejected(new Error(`${i}という数は不吉過ぎます`));
					}
					else if(i>0){
						console.log(i + '...');
					}
					else{
						console.log("GO!");
						onFulfilled();
					}
				}, 
				(seconds-i)*1000));
		}
	});
}

const p = countdown(15);
p.then(function() {
	console.log("カウントダウン成功")
});
p.catch(function(err) {
	console.log("カウントダウンでエラーが起こった:" + err.message);
});

//実行結果
15...
14...
カウントダウンでエラーが起こった:13という数は不吉過ぎます

上記の内容の復習でPromiseを使って処理を待機させる。

上記のコールバック地獄1のコードをpromiseを使って書き換えていく。

function wait(num){
	//引数なしの無名関数をアロー関数で書いてる。
	//と思わせてPromiseではresolve, rejectを引数として取る。
	return new Promise((resolve, reject) => {
		//このアロー関数の中で非同期処理を書いていく。
		setTimeout(() => {
			console.log(num);//最初は0
			//ここが呼ばれた時点で次の処理に移る。callbackと同じ機能エラーを出す時はrejectで呼び出す??
			resolve(num);//wait(num=>{n++; ....},num};ここでnumをインクリメント
		}, num);
	});
}

//waitの処理が終わった後の処理はthenで行う。
//resolveで渡した引数がthenメソッドのコールバック関数の引数となる。
wait(0).then(num => {
	num++;
	//thenメソッドの中の関数の戻り値にPromiseを渡す事でこの処理も非同期で行われる。
	return wait(num);
})

同期処理のように処理が終了してから別の処理を行なわせたい(チェーンする)場合は then を使用する。これはコールバック地獄をより分かりやすい形にする事ができる。処理自体は非同期を同期にする。

function wait(num){
	//引数なしの無名関数をアロー関数で書いてる。
	//と思わせてPromiseではresolve, rejectを引数として取る。
	return new Promise((resolve, reject) => {
		//このアロー関数の中で非同期処理を書いていく。
		setTimeout(() => {
			console.log(num);//最初は0
			//ここが呼ばれた時点で次の処理に移る。callbackと同じ機能エラーを出す時はrejectで呼び出す??
			resolve(num);//wait(num=>{n++; ....},num};ここでnumをインクリメント
		}, 100);
	});
}

//waitの処理が終わった後の処理はthenで行う。
//resolveで渡した引数がthenメソッドのコールバック関数の引数となる。
wait(0).then(num => {
	num++;
	//thenメソッドの中の関数の戻り値にPromiseを渡す事でこの処理も非同期で行われる。
	//戻り値は次のthenメソッドの引数として渡される。
	// returnで返さずにwait(num)と記述するとチェーンが切れ非同期になる。
	return wait(num);
}).then(num => {
		num++;
		return wait(num);
}).then(num => {
		num++;
		return wait(num);
}).then(num => {
		num++;
		return wait(num);
}).then(num => {
		num++;
		return wait(num);
})

//実行結果
0
1
2
3
4

今度はrejectを使用してエラーをハンドリングしていく。

function wait(num){
	//引数なしの無名関数をアロー関数で書いてる。
	//と思わせてPromiseではresolve, rejectを引数として取る。
	return new Promise((resolve, reject) => {
		//このアロー関数の中で非同期処理を書いていく。
		setTimeout(() => {
			console.log(num);//最初は0
			if(num === 2){
				reject(num);
			}else{
				//ここが呼ばれた時点で次の処理に移る。callbackと同じ機能エラーを出す時はrejectで呼び出す??
				resolve(num);//wait(num=>{n++; ....},num};ここでnumをインクリメント
			}
			
		}, 100);
	});
}

//waitの処理が終わった後の処理はthenで行う。
//resolveで渡した引数がthenメソッドのコールバック関数の引数となる。
wait(0).then(num => {
	num++;
	//thenメソッドの中の関数の戻り値にPromiseを渡す事でこの処理も非同期で行われる。
	//戻り値は次のthenメソッドの引数として渡される。
	// returnで返さずにwait(num)と記述するとチェーンが切れ非同期になる。
	return wait(num);
}).then(num => {
		num++;
		return wait(num);
}).then(num => {
		num++;
		return wait(num);
}).then(num => {
		num++;
		return wait(num);
}).then(num => {
		num++;
		return wait(num);
}).catch(num => {
		num++;
		console.error(num, 'error');
});

//実行結果
0
1
2
3 error

プロミスのチェイン

上記の then を使用し複数の非同期処理を順番に実行して、前の処理が完了してからその結果を次の処理で使う。この一連の動作をプロミスのチェインと呼ぶ。これは非同期を同期に処理する事を意味する。これを行うのに昔は上記でも出てきたコールバック地獄を使って実装していた。

doSomething(function(result) {
	doSomethingElse(result, function(newResult) {
		doThirdThing(newResult, function(finalResult) {
			console.log('Got the final result: ' + finalResult);
		}, failureCallback);
	}, failureCallback);
}, failureCallback);

プロミスチェインを使えば

doSomething()
.then(function(result) {
	return doSomethingElse(result);
})
.then(function(newResult) {
	return doThirdThing(newResult);
})
.then(function(finalResult) {
	console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

//アロー関数を使うと
doSomething()
.then(result => return doSomething(result))
.then(newResult => return doThirdThing(newResult))
.then(finalResult) {
	console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

※コールバック関数で処理を返すことを忘れないで下さい。コールバック関数からその処理結果を利用する事ができなくなる。アロー関数の場合は () => x() => {return x;} の省略形で返している。

catch後のチェーン

失敗、つまり catchの後にチェーンすることも可能。チェーン内の動作が失敗した場合新たに処理を始めるのに使用できる。

new Promise((resolve, reject) => {
	console.log('Initial');
	
	resolve();
})
.then(() => {
	throw new Error('Something failed');
	console.log('Do this');
})
.catch(() => {
	console.log('Do that');
})
.then(() => {
	console.log('Do this whatever happened before');
});

チェインを使用してカウントダウン後にロケットを打ち上げるようにする。

function countdown(seconds) {
	return new Promise(function(onFulfilled, onRejected) {
		const timeoutIds =[];
		for(let i=seconds; i>=0; i--) {
			timeoutIds.push(setTimeout(
				function() {
					if(i===13) {
						timeoutIds.forEach(clearTimeout);
						onRejected(new Error(`${i}という数は不吉過ぎます`));
					}
					else if(i>0){
						console.log(i + '...');
					}
					else{
						console.log("GO!");
						onFulfilled();
					}
				}, 
				(seconds-i)*1000));
		}
	});
}

function launch() {
	return new Promise(function(onFulfilled, onRejected) {
		console.log("発射!");
		setTimeout(function() {
			onFulfilled("周回軌道に乗った!");
		}, 2*1000); /*超高速ロケット*/
	});
}

countdown(11)
	.then(launch)
	.then(function(msg) {/*関数launch内のonFulfilledの引数がmsgに渡る*/
		console.log(msg);
	})//カウントダウンが13以上だとonRejectedが実行され下記のcatchに処理が移る。
	.catch(function(err) {
		console.error("管制塔、管制塔。トラブル発生..." + err);
})

//実行結果
11...
10...
9...
8...
7...
6...
5...
4...
3...
2...
1...
GO!
発射!
周回軌道に乗った!

もう一つチェイニングの例を見る。3つのファイル(a.txt, b.txt, c.txt)を非同期に読み込んで、準備ができたところでd.txtに3つのファイル内容を書き込むプログラム。

'use strict';
const fs = require('fs');

function readFile(fileName) {
	return new Promise(
		(onFulfilled, onRejected) => {
			//console.log(data);
			if (err) {
				//console.error("readFile error:" + fileName + err);
				onRejected(err);
			}
			onFulfilled(data);
		});
	});
}

function writeFile(fileName, data) {
	return new Promise(
		(onFulfilled, onRejected) => {
			fs.writeFile(fileName, data, err => {
				if(err) {
					//console.error("writeFile error:" + fileName + err);
					onRejected(err);
				}
				onFulfilled("OK");
			});
		});
}

//ファイルを同期的に読んで行って最後に合体させる。

let allData = "";
readFile("a.txt")
.then(function(fileData) {//a.txtを読み込んだら、allDataに格納して、次にb.txtを読み込む。
	allData += fileData;
	return readFile("b.txt");
})
.then(function(fileData) {
	allData += fileData;
	return readFile("c.txt");
})
.then(function(fileData) {//全てのファイルを読み込んだらallDataをd.txtに書き込む。
	allData += fileData;
	return writeFile("d.txt", allData);
})
.then(function(mes) {
	console.log("ファイルの合体に成功しました。");
})
.catch(err => {
	console.error("エラーが起こりました:" + err);
});

Promise.allの使い方

非同期で処理をたくさん走らせて全ての並列処理が終わったタイミングで何かする場合は Promise.all を使用する。前のプログラムでファイルを同期的に読み込んでいたので、同時に読み込んだ方が効率的になる。それを実現するのに Promise.all を使用する。

Promiseにはallという名前のメソッドがあり、配列内の全プロミスがresolveすると全体がresolveすることになっている。実際に先ほどのファイルを読み込んだプログラムを並列に読み込み、全て揃った所でファイルに書き込むように設定する。ファイルの読み込まれる順番は b.txt が最初になるかも知れないし、 c.txt かも知れないですが、その結果は配列に順番通りに返ってくる。

results[0]a.txt が入る。そしてその中の一つでも失敗している場合は、すぐに値の全体で失敗したことになる。

Promise.race を使用すれば複数の処理に競争させ、もっとも早く成功、あるいは失敗したものが返される。早く処理できたものを採用する事が出来る。

//プロミスに配列で関数を渡す。
Promise.all([readFile("a.txt"), readFile("b.txt"), readFile("c.txt")])
.then(function(results) { //その結果も配列で戻ってくる。
	const allData = results[0] + results[1] + results[2];
	return writeFile("d.txt", allData);
})
.then(function(mes) {
	console.log("ファイルの合体に成功しました。");
})
.catch(err => {
	console.error("エラーが起こりました:" + err);
});

次の例ではファイルの読み込み時間をランダムに送らせて、3つのファイルのうち一つを d.txt に書き込むようにする。0以上1未満の少数をランダムに返す。下記の例ではたとえc.txtが読み込めなくても、全体の処理はエラーにならない。

const fs = require('fs');

function writeFile(fileName, data) {
	return new Promise((onFulfilled, onRejected) => {
		fs.writeFile(fileName, data, err => {
			err ? onRejected(err) : onFulfilled('OK');
		}); });
}

function readFile(fileName) {
	return new Promise((onFulfilled, onRejected) => {
		const period = Math.random()*1000;
		console.log(`${fileName}: ${period}`);
		setTimeout(() => {
			fs.readFile(fileName, "utf-8", (err, data) => {
				err ? onRejected(err) : onFulfilled([fileName, data]);
			});
		}, period);
	});
}

let selected;
Promise.race([readFile("a.txt"), readFile("b.txt"), readFile("c.txt")])
.then(function(results) {
	selected = results[0];
	return writeFile("d.txt", results[1]);
})
.then(function(mes) {
	console.log(`ファイル${selected}の内容が書き込まれました。\n----`);
});
.catch(err => {
	console.error("エラーが起こりました:" + err);
});

Promise.all ブラウザで実行出来る形式のサンプル。

//この関数を非同期で並列に走らせる。
function wait(num){
	//引数なしの無名関数をアロー関数で書いてる。
	//と思わせてPromiseではresolve, rejectを引数として取る。
	return new Promise((resolve, reject) => {
		//このアロー関数の中で非同期処理を書いていく。
		setTimeout(() => {
			console.log(num);//最初は0
			if(num === 2){
				reject(num);
			}else{
				//ここが呼ばれた時点で次の処理に移る。callbackと同じ機能エラーを出す時はrejectで呼び出す??
				resolve(num);//wait(num=>{n++; ....},num};ここでnumをインクリメント
			}
			
		}, num);
	});
}

Promise.all([wait(1000), wait(1500), wait(2000)]).then(nums => {
	console.log(nums)
})

//実行結果
1000
1500
2000
[1000, 1500, 2000]
//全ての処理が実行されたのちに配列が返ってくる。

Promise.race を使ってみる。

race を使って 一つの処理が終わったタイミングで then を呼ぶ事ができる。

Promise.race([wait(1000), wait(1500), wait(2000)]).then(nums => {
	console.log(nums + 1);
})
//実行結果
1000
1001 //ここでthenが呼び出された。numsは一つしか値がないので配列になっていない。
1500
2000

未確定の(unsettled)プロミスを防止する。

プロミスは非同期のコードを単純にしてくれ複数回コールバックが呼ばれてしまう問題を回避してくれるが、その処理は onFulfilled, onRejected も呼ばれない処理の場合、未確定のままエラーも出力しない問題がある。そして全体が複雑になると未確定の問題は出力されないのでわからなくなる。

これを防ぐ方法の一つがプロミスに足してタイムアウトを指定する事。然るべき時間内にプロミスが確定しない場合は、自動的にrejectする。その時間は任意で決める必要がある。長い処理ならそれ以上にタイムアウトは長くする必要がある。

先ほどのロケットのプログラムの launch() 、書き換えて2回に一回は打ち上げに失敗するようにする。

function launch() {
	return new Promise(function(onFulfilled, onRejected) {
		if(Math.random() < 0.5) return; //ここを追加した。
		console.log("発射!");
		setTimeout(function() {
			onFulfilled("周回軌道に乗った!");
		}, 2*1000); /*超高速ロケット*/
	});
}

//失敗した場合の実行結果
3...
2...
1...
GO!

失敗時、 onRejected も呼ばないしメッセージも出力せず単に終了するだけになる。

プロミスにタイムアウトをアタッチする関数addTimeoutを加える。

function addTimeout(fn,
										period
										) {
	if(period === undefined) period = 1000;
	return function(...args) {
		return new Promise(function(onFulfilled, onRejected) {
			//setTimeoutの第3引数は渡した関数の引数になる。
			const timeoutId = setTimeout(onRejected, period, new Error("プロミス タイムアウト"));
				fn(...args)
				.then(function(...args) {
					clearTimeout(timeoutId);
					onFulfilled(...args);
				})
				.catch(function(...args) {//lunch関数はonRejectedを呼び出していないのでここが実行される事はない。
					clearTimeout(timeoutId);
					onRejected(...args);
				});
		});
	}
}

countdown(3)
	.then(addTimeout(launch, 4*1000))
	.then(function(msg) {/*関数launch内のonFulfilledの引数がmsgに渡る*/
		console.log(msg);
	})//カウントダウンが13以上だとonRejectedが実行され下記のcatchに処理が移る。
	.catch(function(err) {
		console.error("管制塔、管制塔。トラブル発生..." + err);
})

//実行結果(Math.ramdomが5以下の場合)
3...
2...
1...
GO!
管制塔、管制塔。トラブル発生...Error: プロミス タイムアウト

上記のコードは50%で 周回軌道に乗った の文字列が出力される。残り50%で 管制塔、管制塔。トラブル発生...プロミス タイムアウト が出力される。引数の ...args は受け取る関数にいかなる引数があっても受け取れるように残余引数というのを指定している。 ... を任意の変数名に付ける事で何個の引数でも受け取れるようになる。空の場合は [] が入る。

ジェネレータ

ジェネレータを使用すると関数とその呼び出し側と双方向のやり取りが可能になります。ジェネレータは本来同期的に動作しますがプロミスと同時に使うとJavaScriptの非同期コードを管理するのに強力なテクニックを使えるようになる。

非同期コードの難しい所をもう一度振り返ると人間は同期的な処理の方が得意である。しかしこれではパフォーマンス上に問題が出るのでこのようなことに対処するのにジェネレータは役に立つ。

「コールバック地獄」の例では3つのファイルを読み込み、しばらく待ってから4番目のファイルに書く。

人間に取っては次の「擬似コードのように順番にやる方が分かりやすい。

ジェネレータを使用すると下記のような擬似コードみたいにコードを実行出来る。

dataA = ファイル'a.txt'を読み込み
dataB = ファイル'b.txt'を読み込み
dataC = ファイル'c.txt'を読み込み
読み込みが完了してからdataA + dataB + dataCを'd.txt'に書き出し

ジェネレータで上記の文を実装していく。それに必要となるのが「ジェネレーターランナー」になる。ジェネレータはもともと非同期ではないが、非同期の呼び出しを扱う方法を知っている関数(ジェネレーターランナー)を作る事が出来る。

function grun(g) {
	const it = g();
	(function iterate(val) {
		const x = it.next(val);//2回目の呼び出し以降で引数がyieldに入る。
		if(!x.done) {//イテレータが残っている時はtrue
			if(x.value instanceof Promise) {//x.valueに最初はPromise関数にしたreadFileを呼び出した。保留のプロミスオブジェクトがで入る。
				//thenでx.valueが読み込んだファイルになるまで待機する。ファイルを見込んだらiterateを再び呼んで再帰的に処理をする。
				//その際に変数に読み込んだファイルを格納する。
				x.value.then(iterate).catch(err => it.throw(err));
			} else {
				setTimeout(iterate, 0, x.value);
			}
		}
	})();
}

function readFile(fileName) {
	return new Promise(
		(onFulfilled, onRejected) => {
			fs.readFile(fileName, 'utf-8',
				(err, data) => err ? onRejected(err) : onFulfilled(data));
			})
}

function writeFile(fileName, data) {
	return new Promise(
		(onFulfilled, onRejected) => {
			fs.writeFile(fileName, data, err => err ? onRejected(err) : onFulfilled("OK"));
			});
}

function* fileReadAndWrite() {
	const dataA = yield readFile('a.txt');
	const dataB = yield readFile('b.txt');
	const dataC = yield readFile('c.txt');
	yield writeFile('d.txt', dataA+dataB+dataC);
}

grun(fileReadAndWrite);

今度は上記の処理を並列で処理していこうと思う。今は非同期処理をジェネレータで制御して同期的にファイルを読み込むようした。それを今度は Promisse.all を使ってファイルを読み込む段階までは非同期(並列)で処理して全てのファイルが読み込まれたら、それを足して出力する処理を書いていく。こうする事でファイル読み込みは同時に読み込まれ効率が上がる。

dataA = ファイル'a.txt'を読み込み
dataB = ファイル'b.txt'を読み込み
dataC = ファイル'c.txt'を読み込み
読み込みが完了してからdataA + dataB + dataCを'd.txt'に書き出し
function* fileReadAndWrite() {
	const data = yield Promise.all([readFile('a.txt').
																	readFile('b.txt'), readFile('c.txt')]);
	yield writeFile('d.txt, data[0]+data[1]+data[2]');
}

grun(fileReadAndWrite);

ジェネレータランナーの例外処理

上記のコードに例外処理を追加する。

function* fileReadAndWrite() {
	try {
		const data = yield Promise.all([readFile('a.txt').
																		readFile('b.txt'), readFile('c.txt')]);
		yield writeFile('d.txt, data[0]+data[1]+data[2]');
	} catch (err) {
		console.error("エラーが起こりました:" + err);
	}
}

grun(fileReadAndWrite);

await asyncを使った非同期

ルール

  1. awaitを付けると戻り値が返るまで待機する。
  2. awaitを使用した関数の先頭にasyncを付ける。この関数は非同期であると示す。
async function sample() {
	//awaitを付ける事でasyncFn()の戻り値が来るまでnum++は実行されない。
	
	let num = await asyncFn();
	num++;
	return num;
}

//上記のコードをプロミスで記述する場合
//asyncFn()が実行された時点でthenメソッドが呼ばれる。
asyncFn(0).then(num => {
	num++;
	return num;
})

先ほど使用したPromiseの関数async awaitで非同期にする。


function wait(num){
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			console.log(num);
			if(num === 2){
				reject(num);
			}else{
				resolve(num);
			}
		}, 100);
	});
}

async function init(){
	let num = 0
	try{
		num = await wait(num);
		num++;
		num = await wait(num);
		num++;

	}catch(e){
		throw new Error('Error is occured', e);

	}
	return num;//Promiseでラップされた値が返る。
}

init();//戻り値がPromiseなのでそのままthenメソッドが使用できる。

参照

1)Ethan Brown. Learning JavaScript, 3rd Edition. O'Reilly. イーサン ブラウン ムシャ ヒロユキ ムシャ ルミ (訳) 2017. 「14章 非同期プログラミング」.『初めてのJavascript』. 第3版. オライリージャパン. pp 229-256.

下記の動画を学習しながら、疑問に思った事をまとめて記事にしました。この方udemyで講師をしている方で動画がとても丁寧で分かりやすい(しかも無料!!)のでJavascriptで非同期を学ぶなら絶対おすすめです。

【JavaScript】非同期操作について学ぼう1(コールバック関数)

【JavaScript】非同期操作について学ぼう2(Promise関数)

【JavaScript】非同期操作について学ぼう3(Await/Async関数)

記事に関するコメント等は

🕊:Twitter
📺:Youtube
📸:Instagram
👨🏻‍💻:Github
😥:Stackoverflow

でも受け付けています。どこかにはいます。

Discussion