非同期処理が難しい
JavaScriptを勉強して5ヶ月が経ち、なんとなく使っていたpromiseとasync/awaitに躓き始めました。
非同期処理に欠かせないPromiseを今一度自分の中で整理するためにこの記事を描きました。
Promiseとは?
大まかにいうと、非同期処理を可読性が上がるように書けるようにしたものです。
???
非同期処理とは?、可読性が上がる?
1.1同期処理と非同期処理
同期処理
同期処理とはメインスレッドでコードが順番に実行されることです。
イメージとしては、
こんな感じです。
一つの処理が完了するまで次の処理には進みません。
もし、処理2が10000秒かかる重たい処理であれば処理3は10000秒待たなければなりません。(めっちゃ大袈裟に言ってます)
それでは困りますよね?そこで非同期処理というものがあります。
非同期処理
非同期処理は一時的にメインスレッドから処理が切り離される処理です。
こんな感じですかね
非同期処理によって時間がかかるような処理がある場合もその他の処理が止まることがなく動かすことができます。
JavaScriptの非同期処理の文法としてコールバック,Promise,async/awaitがあります。
ここでPromiseとasync/await というワードが出てくるわけです。
1.2 Promiseを使わない書き方
PromiseはES2015で追加された書き方であり、昔から使われてはいない文法でした。ではPromiseを使わない非同期処理の書き方はどういうものがあるのでしょう?
setTimeoutという非同期処理が行える関数で書いてみます。
setTimeoutは,指定した時間の後に処理を実行してくれるものです。
非同期なので
function a() {
console.log(1);
}
function b() {
setTimeout(() => {
console.log(2);
}, 1000);
}
function c() {
console.log(3);
}
a();
b();
c();
結果
1
3
2
となります。
もし非同期処理が終わった後に次の処理を走らせたい場合
setTimeout(function () {
console.log(1);
setTimeout(function () {
console.log(2);
setTimeout(function () {
console.log(3);
}, 1000);
},1000);
}, 1000);
結果
1
2
3
入れ子構造で見づらいですよね
もっと見やすく書く為にあるのがPromise
2.1 Promise
Promiseは非同期処理が完了したあと適切な次の処理を実行することを約束してくれます。
非同期処理成功時は成功パターンの処理を失敗時は失敗パターンの処理を走らせる事ができます。
その為、Promiseは状態を持っています。
- 待機(pending):初期状態。成功も失敗もしていません。
- 履行(fullfilled):処理が成功して完了したことを意味します。
- 拒否(rejected):処理が失敗したことを意味します。
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('foo');
}, 300);
})
newPromiseと書いてコールバック関数の第一引数にresolve,
第二引数にrejectを書きます。Promise内でresolveが発火されると成功(状態がfullfilledになる)、rejectが発火されると失敗(状態がrejectedになる)になります。
2.2 Promiseで非同期処理を連鎖させる
Promiseは帰ってきた状態を見て成功パターンと失敗パターンの処理を走らせます。
then
fullfilledの状態になった時にthenの中の処理が走ります
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('foo');
}, 300);
}).then((result)=>{
console.log(result)
})
結果
foo
resolveの中の値がthenのコールバック関数の引数として入ってきて
thenの引数resultの値はfooです。
catch
rejectedの状態になった時catchの中の処理が走ります
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject("エラーです");
}, 300);
})
.then((result) => {
console.log(result);
})
.catch((err) => {
console.error(err);
});
結果
エラーです
rejectがPromiseの中で発火されるとcatchに処理が移りthenの中の処理は行われません.
finally
最後にfinallyというものもあります。
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject("エラーです");
}, 300);
})
.then((result) => {
console.log(result);
})
.catch((err) => {
console.error(err);
})
.finally(() => {
console.log("処理終了");
});
結果
エラーです
処理終了
こちらは処理の成功、失敗に関わらずthenかcatchの後に中身が処理されます。今回はcatchパターンで書いていますが、thenの後でも処理されます。
2.3 1.3の処理をPromiseで書いてみた
new Promise(resolve => {
setTimeout(() => {
console.log(1);
resolve();
}, 1000);
})
.then(() => {
return new Promise(resolve => {
setTimeout(() => {
console.log(2);
resolve();
}, 1000);
});
})
.then(() => {
return new Promise(resolve => {
setTimeout(() => {
console.log(3);
resolve();
}, 1000);
});
});
結果
1
2
3
入れ子構造じゃなくなっていますね
読みやすくなった!
3.1 Promiseをもっとスマートに書きたい~async/await~
入れ子構造ではなくなり読みやすくはなったのですが、まだちょっと見づらいですよね、なんか長いし。もっとスマートにしたい...
それを実現するasync/await
- async 非同期関数を定義する関数宣言でありPromiseObjectを返す
- await promiseオブジェクトが値を返すのを待つ演算子
というものです。
const myPromise = () => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("foo");
resolve();
}, 300);
});
};
async function sayFoo() {
await myPromise();
console.log("async/awaitしたよ")
}
sayFoo();
結果
foo
async/awaitしたよ
awaitの後ろに書いてあるPromiseObjectが完了するのを待ってconsole.logの処理をしています。
1.3と結果が同じ処理を書くと
async function log() {
await myPromise(1);
await myPromise(2);
await myPromise(3);
}
const myPromise = number => {
return new Promise(resolve => {
setTimeout(() => {
console.log(number);
resolve();
}, 1000);
})
};
log();
結果
1
2
3
見やすいですね
3.2 async/awaitの失敗時は?
上記の例だとrejectedした時の処理がないですね
try,catchを用いります。
async function log() {
try {
await myPromise(1);
await errPromise(2);
await myPromise(3);
} catch (err) {
console.log(err);
}
}
const myPromise = (number) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(number);
resolve();
}, 1000);
});
};
const errPromise = (number) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(`${number}番目エラーだよ`);
}, 1000);
});
};
log();
結果
1
2番目エラーだよ
awaitの2つ目でrejectが返ってくるPromiseを発火しているのでcatchに処理が移りました。3つ目のawaitは処理されません。
4 まとめ
コードの可読性を良くするためのJSの進化がわかりました。自分は今までなんとなくthenやらasync/awaitをごちゃ混ぜにして書いていたので今後気をつけます。
参考
山浦さんのYouTube(めっちゃわかりやすかったです)
小学生でもわかるasync/await/Promise入門【JavaScript講座】