前提
以降の説明では下記の非同期関数を使用して説明を実施します。
※使用したソースは全て GitHub に上げてあります
file.js
function readFile(fileName, success) {
setTimeout(() => {
if (typeof(success) === 'function') {
success(fileName)
}
}, 500)
}
function readFileAsync(fileName) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(fileName), 500)
})
}
プログラムの常識
コードは上から順に実行される
sync.js
const hoge = readFile('hoge.txt')
const fuga = readFile('fuga.txt')
const foo = readFile('foo.txt')
const bar = readFile('bar.txt')
console.log(hoge + fuga + foo + bar)
動かない
なんで?
- 非同期処理だから
- ファイルの読み込みが終わる前に
console.log()
が走っちゃう
なんで非同期なの?
- 同期的な処理だと、IOアクセスの待ち時間中に何もできなくなる
- DOMを操作するときはなにかと非同期で動かしたいことが多い(Ajaxのレスポンスを待っている間にプログレスバーを表示させるとか)
ES6以前の非同期処理
- コールバック地獄
コールバック地獄
async.js
readFile('hoge.txt', function (hoge) {
readFile('fuga.txt', function (fuga) {
readFile('foo.txt', function (foo) {
readFile('bar.txt', function (bar) {
console.log(hoge + fuga + foo + bar)
})
})
})
})
- 深いネスト
- エラーをまとめて受け取れない
- function ,function , function ...
ES6 (ECMAScript 2015)以後の世界
Promise
Promiseとは
Promiseは非同期処理を抽象化したオブジェクトとそれを操作する仕組みです。
使いこなすことで、以下のような効果が見込めます。
- コールバック地獄から解放される
- エラー処理をうまく記述できる
- 一連の非同期処理を関数化して再利用しやすくできる
書き方
howto.js
const promise = function(str) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(str), 500)
})
}
promise('foo')
.then(
(val) => {
console.log(val)
return promise('bar')
})
.then((val) => console.log(val))
.catch((err) => console.log(err))
解説
Promiseオブジェクト
- Promiseオブジェクトを返す関数を書く
- Promiseオブジェクトの中に非同期に行いたい処理を書く
- 非同期処理が終わったらresolve()を呼び出す
- 非同期処理が失敗したらreject()を呼び出す
解説
Promiseオブジェクトを使う
- 非同期処理を実行したあとに実行したい処理をthen節の中に書く
- 実行結果は引数(val)で受け取る
- 非同期処理が失敗したらcatch節で受け取れる
いいところ
- ネストが深くならない
- 各処理で起こったエラー(例外)をcatch節でまとめて補足できる
これでコールバックともおさらばだぜ!!
promise.js
let hoge, fuga, foo, bar
readFileAsync('hoge.txt')
.then((result) => {
hoge = result
return readFileAsync('fuga.txt')
})
.then((result) => {
fuga = result
return readFileAsync('foo.txt')
})
.then((result) => {
foo = result
return readFileAsync('bar.txt')
})
.then((result) => {
bar = result
console.log(hoge + fuga + foo + bar)
})
うーん・・・
このままでもできるけど・・・
- 確かにネストは浅くなったが長い
- 外の変数に退避しないと一個前の結果を参照できない
- then,then,then...
もっと同期的に書きたい
理想のコード.js
const hoge = readFileAsync('hoge.txt')
const fuga = readFileAsync('fuga.txt')
const foo = readFileAsync('foo.txt')
const bar = readFileAsync('bar.txt')
console.log(hoge + fuga + foo + bar)
そして ES8(ECMAScript 2017)へ・・・
async/awaitの登場
await.js
async function readFiles(){
const hoge = await readFileAsync('hoge.txt')
const fuga = await readFileAsync('fuga.txt')
const foo = await readFileAsync('foo.txt')
const bar = await readFileAsync('bar.txt')
console.log(hoge + fuga + foo + bar)
}
readFiles()
解説
- 同期的に処理したい部分を
async function
で囲む - 同期的に処理したい非同期処理関数の手前に
await
をつける - Promiseの処理が終わるまで待ってくれるようになる
ちなみに・・・
- async functionもPromiseを返すので、以下のようなコードは非同期に実行されます
await1.js
async function readFiles(){
const hoge = await readFileAsync('hoge.txt')
const fuga = await readFileAsync('fuga.txt')
const foo = await readFileAsync('foo.txt')
const bar = await readFileAsync('bar.txt')
console.log(hoge + fuga + foo + bar)
}
readFiles()
console.log('readFiles()はまだ終わらない・・・')
- ちゃんとやるなら以下のような感じ
await2.js
async function readFiles() {
const hoge = await readFileAsync('hoge.txt')
const fuga = await readFileAsync('fuga.txt')
const foo = await readFileAsync('foo.txt')
const bar = await readFileAsync('bar.txt')
console.log(hoge + fuga + foo + bar)
}
readFiles().then(() => console.log('readFiles()は終わった!!'))
めでたしめでたし
ちょっとまって
直列処理はいいけど並列処理は?
Promise.all()
Promise.all()
- 複数の非同期処理が完了した後にthen節を呼び出せる
promise_all.js
const hoge = readFileAsync('hoge.txt')
const fuga = readFileAsync('fuga.txt')
const foo = readFileAsync('foo.txt')
const bar = readFileAsync('bar.txt')
Promise.all([hoge, fuga, foo, bar])
.then((values) => console.log(values[0] + values[1] + values[2] + values[3]))
awaitとの合わせ技も可能
promise_all1.js
async function readFiles() {
const values = await Promise.all([
readFileAsync('hoge.txt'),
readFileAsync('fuga.txt'),
readFileAsync('foo.txt'),
readFileAsync('bar.txt'),
])
console.log(values[0] + values[1] + values[2] + values[3])
}
readFiles()
まとめ
- async/awaitは神
- Promise.allとかも使って、非同期にすべきところは非同期にしよう
- ちなみに新しい規格なのでIE非対応。使いたいときはトランスコンパイルしましょう
補足資料
ES6,ES8
トランスパイルとは
トランスコンパイラ(他にトランスパイラ、ソース・トゥ・ソースコンパイラ、などとも)は、あるプログラミング言語で書かれたプログラムのソースコードを入力として受け取り、別のプログラミング言語の同等のコードを目的コードとして生成する、ある種のコンパイラである
-- Wikipediaより