はじめに
趣味のプログラミングで JavaScript を使い始めて随分になります。ところが、非同期処理や Promise になかなか慣れません。そろそろきちんと使えるようにしておきたいと思いました。
非同期関数を順に呼出する
例えば、3 つのファイルを順に読込して処理したいとします。以下のコードを書きます。
import fs from 'fs';
fs.readFile("ファイル1", "utf-8", (err, data) => {
console.log("readFile (1) finished.");
});
fs.readFile("ファイル2", "utf-8", (err, data) => {
console.log("readFile (2) finished.");
});
fs.readFile("ファイル3", "utf-8", (err, data) => {
console.log("readFile (3) finished.");
});
fs.readFile()
は非同期関数であるため、「ファイル1」の読込が終わってから「ファイル2」続いて「ファイル3」の読込・・となりませんね。順に読込するにはどうしたらよかったでしょうか。
コールバックで対応する
最初の呼出のコールバックで次の呼出して、そのコールバックで次の呼出して・・とします。
fs.readFile("ファイル1", "utf-8", (err, data) => {
console.log("readFile (1) finished.");
fs.readFile("ファイル2", "utf-8", (err, data) => {
console.log("readFile (2) finished.");
fs.readFile("ファイル3", "utf-8", (err, data) => {
console.log("readFile (3) finished.");
});
});
});
順に実行したい処理が増えると、コールバックのネストが深くなり、読みづらくなる欠点が指摘されていますね。
async.js で対応する
サードパーティのライブラリ async.js
(2012 年に公開)を使います。
caolan/async: Async utilities for node and the browser
import async from 'async';
async.series([
(done) => {
fs.readFile("ファイル1", "utf-8", (err, data) => {
console.log("readFile (1) finished.");
done(err, null);
});
},
(done) => {
fs.readFile("ファイル2", "utf-8", (err, data) => {
console.log("readFile (2) finished.");
done(err, null);
});
},
(done) => {
fs.readFile("ファイル3", "utf-8", (err, data) => {
console.log("readFile (3) finished.");
done(err, null);
});
}
], (err, results) => {
if (err) console.log(err);
});
コード量は増えてしまいましたが、順に実行するのが分かりやすく書けています。
jQuery で対応する
サードパーティのライブラリ jQuery
に Deferred
が用意されました(2011 年)。
後述する Promise
が用意されたため、今になって Deferred
を使うことはないでしょう。
Promise で対応する
JavaScript 言語に Promise
が用意されました(2015 年)。これを使います。
new Promise((resolve, reject) => {
fs.readFile("ファイル1", "utf-8", (err, data) => {
console.log("readFile (1) finished.");
resolve();
});
})
.then((resolve, reject) => {
return new Promise((resolve, reject) => {
fs.readFile("ファイル2", "utf-8", (err, data) => {
console.log("readFile (2) finished.");
resolve();
});
});
})
.then((resolve, reject) => {
return new Promise((resolve, reject) => {
fs.readFile("ファイル3", "utf-8", (err, data) => {
console.log("readFile (3) finished.");
resolve();
});
});
});
最初の呼出と以降の呼出のネストが違うのが気になりますね。↑
以下のようにしてもいいようです。↓
Promise.resolve()
.then(() => {
return new Promise((resolve, reject) => {
fs.readFile("ファイル1", "utf-8", (err, data) => {
console.log("readFile (1) finished.");
resolve();
});
});
})
.then((resolve, reject) => {
return new Promise((resolve, reject) => {
fs.readFile("ファイル2", "utf-8", (err, data) => {
(以下略)
非同期関数を順に呼出できたけれど
非同期関数を順に呼出できるようになりました。ところが、注意しないといけない点があります。
new Promise((resolve, reject) => {
fs.readFile("ファイル1", "utf-8", (err, data) => {
console.log("readFile (1) finished.");
resolve();
});
})
(中略)
.then((resolve, reject) => {
return new Promise((resolve, reject) => {
fs.readFile("ファイル3", "utf-8", (err, data) => {
console.log("readFile (3) finished.");
resolve();
});
});
});
console.log("all functions finished.")
実行すると
all functions finished.
readFile (1) finished.
readFile (2) finished.
readFile (3) finished.
呼出の後に書かれた処理が先に実行されます。↑
new Promise() ~ then()
は、関数を呼出して、終了したときの処理を予約します。予約し終えると終了を待たずに、次の処理に移るわけです。
非同期関数を呼出する
関数を順に実行するだけなのに、随分と面倒なことをしないといけませんね。
同期関数なら、こんな面倒なことはありません。非同期関数は、なぜ欲しいのでしょう。
前述の fs.reafFile()
は、同期処理できる fs.readFileSync()
が用意されています。これを使うと
const data = fs.readFileSync("ファイル", "utf-8");
console.log("readFileSync finished.");
これを入力画面から呼出すとします。(以下のコードはイメージです。)
●●●●.addEventListner('○○○○', () => {
const data = fs.readFileSync("ファイル", "utf-8");
console.log("readFileSync finished.");
});
ファイルが大きくて読込に時間が掛かると、これを呼出した入力画面は、処理が終わるまでフリーズします。プログラムがハングアップしたように見えるでしょう。
これを解消するために、非同期で処理される関数を呼出するようにします。
●●●●.addEventListner('○○○○', () => {
fs.readFile("ファイル", "utf-8", (err, data) => {
console.log("readFile finished.");
});
});
呼出自体は直ぐに完了するので、呼出した側の入力画面の制御に戻ってきます。
非同期関数を作成する
fs.readFile()
のように非同期処理できる関数を、作成できるでしょうか。
まず、同期処理する関数を作成して呼出してみます。
function funcSync() {
時間の掛かる処理
const result = "result of funcAsync";
return (result);
}
●●●●.addEventListner('○○○○', () => {
const result = funcSync();
console.log("function finished.", result);
});
Promise を返す関数を作成する
JavaScript に用意(2015 年)された Promise
を使います。
function funcAsync() {
return new Promise((resolve, reject) => {
時間の掛かる処理
const result = "result of funcAsync";
resolve(result);
});
}
●●●●.addEventListner('○○○○', () => {
funcAsync()
.then((result) => {
console.log("function finished.", result);
});
});
async を使って関数を作成する
JavaScript に用意(2017 年)された async
を使います。
async function funcAsync() {
時間の掛かる処理
const result = "result of funcAsync";
return (result);
}
(以下略)
return new Promise() ~ resolve()
を async
で書換しています。呼出する側は同じです。
await を使って関数を呼出する
async
と併せて用意された await
を使います。
(前略)
●●●●.addEventListner('○○○○', async () => {
const result = await funcAsync();
console.log("function finished.", result);
});
関数呼出.then()
を async ~ await
で書換しています。呼出される側は同じです。
await を使うとよかったこと
async ~ await
を使うことで、コード量も減って見やすくなります。
関数の定義に async
をつけるので、非同期で実行されることが分かります。
呼出する側は 関数呼出.then()
でもいいのですが、await
を使うといいことがありました。IDE などでブレークポイントを設定してステップ実行したときです。
funcAsync() ⇐ここにブレークポイントを設定
.then((result) => {
console.log("function finished.", result);
});
次の処理 ⇐ステップ実行するとここに来る
const result = await funcAsync(); ⇐ここにブレークポイントを設定
console.log("function finished.", result); ⇐ステップ実行するとここに来る
次の処理
呼出して終了までステップ実行したいとき、then()
の内にブレークポイントを設定しないといけなかったのが、必要なくなりました。
呼出された関数で問題あったとき
呼出された関数で処理していて問題あったとき、それを呼出した側に返すのはどうしたらいいでしょうか。
Promise で reject() する
そのために Promise
に reject()
が用意されています。
function funcResolve() {
return new Promise((resolve, reject) => {
var result = "result funcResolve";
resolve(result); // 処理に問題なし
});
}
function funcReject() {
return new Promise((resolve, reject) => {
var result = "result funcReject";
reject("error funcReject"); // 処理に問題あり
});
}
funcResolve()
.then((result) => {
console.log("function finished.", result);
});
funcReject()
.then((result) => {
console.log("function finished.", result);
});
これを実行すると
function finished. result funcResolve
Uncaught UnhandledPromiseRejection UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "result funcReject".
resolve()
が実行されたときは then()
で、reject()
が実行されたときは catch()
で受取するようになっています。
(前略)
funcReject()
.then((result) => {
console.log("function succeeded.", result);
})
.catch((error) => {
console.log("function failed.", error)
});
実行すると
function failed. error funcReject
async 関数で throw する
上記の処理を async
で書くときはどうなるでしょうか。
async function funcResolve() {
const result = "result funcResolve";
return result;
}
async function funcReject() {
const result = "result funcReject";
throw new Error("error funcReject"); // 処理に問題あり
}
(以下略)
呼出される関数で throw
するようです。このときは Error
オブジェクトを生成して返すのが定番のようです。↑
(前略)
funcReject()
.then((result) => {
console.log("function succeeded.", result);
})
.catch((err) => {
console.log("function failed.", err.message)
});
実行すると
function failed. error funcReject
呼出する側で catch()
で受取できています。Error
オブジェクトを throw
しているので、それを受取しています。
try ~ catch で受取する
関数呼出.then()
でなく await
を使うとどうなるでしょうか。
(前略)
let result = await funcResolve();
console.log("function finished.", result);
result = await funcReject();
console.log("function finished.", result);
これを実行すると
function finished. result funcResolve
Uncaught Error Error: error funcReject
throw
したときは try ~ catch
しないといけませんね。
(前略)
try {
let result = await funcReject();
console.log("function succeeded.", result);
}
catch(err) {
console.log("function failed.", err.message);
}
これを実行すると
function failed. error funcReject
調べていくと、await ~ catch
の書き方もあるようです。
(前略)
let result = await funcReject()
.catch((err) => {
console.log("function failed.", err.message);
return "function failed";
});
console.log("function finished.", result);
実行すると
function failed. error funcReject
function finished. function failed
ところで、throw
して then() ~ catch()
で受取できましたが、reject()
して try ~ catch
できるでしょうか。
function funcReject() {
return new Promise((resolve, reject) => {
var result = "result funcReject";
reject("error funcReject"); // 処理に問題あり
});
}
(前略)
try {
let result = await funcReject();
console.log("function succeeded.", result);
}
catch(error) {
console.log("function failed.", error);
}
実行すると
function failed. error funcReject
呼出される関数が Promise
で実装されているか async
で実装されているか気にしないで、呼出する側も then() ~ catch()
でも try ~ catch
でも実装できますね。