Edited at

JavaScriptと非同期処理の話

More than 1 year has passed since last update.

※身内での勉強会の資料です

使ったソースは全部GitHubに上げてあります


環境

$ node -v

v8.9.3



JavaScriptの歴史、それは非同期処理との戦いの歴史



プログラムの常識

コードは上から順に実行される



sync.js

const fs = require('fs');

var hoge = fs.readFile("hoge.txt")
var fuga = fs.readFile("fuga.txt")
var foo = fs.readFile("foo.txt")
var bar = fs.readFile("bar.txt")
console.log(hoge + fuga + foo + bar);




動かない



なんで?


  • 非同期処理だから


  • ファイルの読み込みが終わる前に console.log() が走っちゃう




なんで非同期なの?


  • 同期的な処理だと、IOアクセスの待ち時間中に何もできなくなる

  • DOMを操作するときはなにかと非同期で動かしたいことが多い(Ajaxのレスポンスを待っている間にプログレスバーを表示させるとか)



ES6以前の非同期処理


コールバック地獄


async.js

const fs = require('fs');

fs.readFile("hoge.txt", function (err, hoge) {
fs.readFile("fuga.txt", function (err, fuga) {
fs.readFile("foo.txt", function (err, foo) {
fs.readFile("bar.txt", function (err, bar) {
console.log(hoge + fuga + foo + bar);
});
});
});
});




  • 深いネスト


  • エラーをまとめて受け取れない


  • function ,function , function ...




ES6 (ECMAScript 2015)以後の世界



Promise



Promiseは非同期処理を抽象化したオブジェクトとそれを操作する仕組みのことをいいます。

-- Promiseの本より




書き方


promise.js

var promise = (str) => {

return new Promise(function (resolve, reject) {
setTimeout(resolve, 100, str);
})
};

promise("foo").then(
(val) => {
console.log(val)
return promise("bar")
}
).then(
(val) => { console.log(val) }
).catch(
(err) => { console.log(err) }
)




解説


Promiseオブジェクト


  • Promiseオブジェクトを返す関数を書く

  • Promiseオブジェクトの中に非同期に行いたい処理を書く

  • Promise関数内の処理が終わったらresolve()を呼び出す

  • Promise関数内の処理が失敗したらreject()を呼び出す



解説


Promiseオブジェクトを使う


  • promiseを実行したあとに実行したい処理をthen節の中に書く

  • 実行結果はvalで受け取る

  • promiseの処理が失敗したらcatch節で受け取れる



いいところ


  • ネストが深くならない

  • 各処理で起こったエラー(例外)をcatch節でまとめて補足できる



これでコールバックともおさらばだぜ!!



promise.js

const fs = require('fs')

const util = require('util')

const readFileAsync = util.promisify(fs.readFile)

var 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) }
)



補足


  • コールバック関数をPromiseに変換できるpromisifyってライブラリがある


  • fs.readFile()はコールバック関数なので今回はこれを使って readFileAsync って関数を作ってます




うーん、微妙・・・



  • 確かにネストは浅くなった

  • でも長い

  • 外の変数に退避しないと一個前の結果を参照できない

  • then,then,then...



もっと同期的に書きたい


理想のコード.js

const fs = require('fs');

var hoge = fs.readFile("hoge.txt")
var fuga = fs.readFile("fuga.txt")
var foo = fs.readFile("foo.txt")
var bar = fs.readFile("bar.txt")
console.log(hoge + fuga + foo + bar);




そして ES8(ES2017)へ・・・



async/awaitの登場



await.js

const fs = require('fs');

const util = require('util');

const readFileAsync = util.promisify(fs.readFile);

async function readFiles(){
var hoge = await readFileAsync("hoge.txt")
var fuga = await readFileAsync("fuga.txt")
var foo = await readFileAsync("foo.txt")
var bar = await readFileAsync("bar.txt")
console.log(hoge + fuga + foo + foo);
}

readFiles()




よい



解説


  • 同期的に処理したい部分を async function で囲む

  • 同期的に処理したいPromise関数の手前に await をつける

  • Promiseの処理が終わるまで待ってくれるようになる



ちなみに・・・


  • async functionもPromiseを返すので、以下のようなコードは非同期に実行されます


await1.js

const fs = require('fs');

const util = require('util');

const readFileAsync = util.promisify(fs.readFile);

async function readFiles(){
var hoge = await readFileAsync("hoge.txt")
var fuga = await readFileAsync("fuga.txt")
var foo = await readFileAsync("foo.txt")
var bar = await readFileAsync("bar.txt")
console.log(hoge + fuga + foo + bar);
}

readFiles()

console.log("readFiles()はまだ終わらない・・・")



  • ちゃんとやるなら以下のような感じ


await2.js

const fs = require('fs');

const util = require('util');

const readFileAsync = util.promisify(fs.readFile);

async function readFiles() {
var hoge = await readFileAsync("hoge.txt")
var fuga = await readFileAsync("fuga.txt")
var foo = await readFileAsync("foo.txt")
var bar = await readFileAsync("bar.txt")
console.log(hoge + fuga + foo + bar);
}

readFiles().then(
() => { console.log("readFiles()は終わった!!") }
)




めでたしめでたし



ちょっとまって



そもそも各ファイルを同期的に読み込む必要ないよね?



なんで非同期なの?


  • 同期的な処理だと、IOアクセスの待ち時間中に何もできなくなる

⇛ 非同期で実装されているにはそれなりの理由がある



Promise.all()



Promise.all()


  • 複数の非同期処理をまとめて実行したあとにthen節を呼び出せる



promise_all.js

const fs = require('fs');

const util = require('util');

const readFileAsync = util.promisify(fs.readFile);

var hoge = readFileAsync("hoge.txt")
var fuga = readFileAsync("fuga.txt")
var foo = readFileAsync("foo.txt")
var 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

const fs = require('fs');

const util = require('util');

const readFileAsync = util.promisify(fs.readFile);

async function readFiles() {
var hoge = readFileAsync("hoge.txt")
var fuga = readFileAsync("fuga.txt")
var foo = readFileAsync("foo.txt")
var bar = readFileAsync("bar.txt")

var values = await Promise.all([hoge, fuga, foo, bar])
console.log(values[0] + values[1] + values[2] + values[3])
}

readFiles()




まとめ


  • async/awaitは神

  • Promise.allとかも使って、非同期にすべきところは非同期にしよう

  • ちなみに新しい規格なのでIE非対応(えっ・・・)。使いたいときはトランスコンパイルしましょう



おしまい