Node.jsを学ぶ-The definitive Node.js handbook-5
【所要時間】
2時間6分(2018年 12月10日)
【概要】
- The definitive Node.js handbookを参考にNode.jsを学ぶ。
- Node.jsを学ぶ-The definitive Node.js handbook- 4の続き。
【要約・学んだこと】
Understanding setImmediate()
コードを非同期的に実行したい、しかし可能な限り早く実行したいときは、Node.jsに提供されているsetImmediate()関数というものが一つの選択肢としてある。
setImmediate(() => {
//run something
})
setImmediate()引数として渡された関数は、イベントループの次の反復で実行されるコールバックとなる。
0msタイムアウトでわたされるsetTimeout(() => {}, 0)
や、process.nextTick()との違いはなにか。
process.nextTick()は現在の処理が終わると、現在のイベントループの反復で実行される。つまり常にsetTimeout()とsetImmediate()より前に実行される。
0ms遅れで設定したsetTimeout()コールバックは、setImmediate()と非常に似ている。実行順は様々な要素に依存するが、どちらも次のイベントループの反復で実行される。
Timers
JSコードを書く時、関数の実行を遅らせたい時がある。setTimeout()とsetInterval()の使い方を説明する。
setTimeout()
JSを書くときに関数の実行を遅らせたい時はsetTimeoutを使う。
setTimeout(() => {
// runs after 2 seconds
}, 2000)setTimeout(() => {
// runs after 50 milliseconds
}, 50)
このシンタックスは新しい関数を定義する。この関数内でいかなる関数も実行でき、また存在する関数名をわたすこと、パラメーターをセットすることができる。
const myFunction = (firstParam, secondParam) => {
// do something
}// runs after 2 seconds
setTimeout(myFunction, 2000, firstParam, secondParam)
setTimeout()はtimer idを返す。これは通常使われないが、このidを格納することができ、関数の実行予定を削除したければクリアできる。
const id = setTimeout(() => {
// should run after 2 seconds
}, 2000)// I changed my mind
clearTimeout(id)
Zero delay
タイムアウトを0とするなら、コールバック関数はすぐに実行されるが、現在の関数の実行が終わった直後になる。
setTimeout(() => {
console.log('after ')
}, 0)console.log(' before ')
これの結果はbefore afterとなる。
これは激しいタスクでCPUをブロックすることを避けるのに有効だ。そしてスケジューラー内の関数がキューすることによって、思い計算をしながら他の関数を実行することができる。
IEやEdgeなどのブラウザでは、setImmediate()メソッドと同じ機能として実行されるが、標準ではない。しかし、Node.js内では標準の関数だ。
setInterval()
setTimeout()と似た機能だが違いはある。コールバック関数の実行が一度ではなく、指定した間隔で無限に実行される。
setInterval(() => {
// runs every 2 seconds
}, 2000)
この関数はclearIntervalで止めるように指示するまで2秒間隔で実行する。setIntervalが返すinterval idを渡す。
const id = setInterval(() => {
// runs every 2 seconds
}, 2000)clearInterval(id)
setIntervalコールバック関数内部でclearIntervalをコールするのが一般的だ。サイド実行するべきか止めるべきかを自動て決定するようにするのだ。例えば、下記のコードはApp.somethingIWaitがarrivedという値を持たない限り実行する。
const interval = setInterval(function() {
if (App.somethingIWait === 'arrived') {
clearInterval(interval)// otherwise do things
}
}, 100)
Recursive setTimeout
setIntervalは関数がいつ実行を終了したかを考えずにnミリ秒ごとに関数を始める。
もし関数が常に同じ時間がかかるなら問題ない。
関数の実行時間が異なるかもしれないなら、ネットワークの状態に依存する。
そして長くなった場合に次の関数の実行と重なることもある。
これをさけるために、コールバック関数が終わった時にコールされる再帰的なsetTimeoutをスケジュールすることができる。
const myFunction = () => {
// do somethingsetTimeout(myFunction, 1000)
}setTimeout(
myFunction()
}, 1000)
setTimeoutとsetIntervalはTimers moduleを通じてNode.js内でも使うことができる。
Node.jsはsetTimeout(() => {}, 0)と同等に使えるsetImmediate()も提供していて、Node.js イベントループで動作するために使用される。
Asynchronous Programming and Callbacks
JSはデフォルトで同期し、それはシングルスレッドだ。つまりコードは新しいスレッドを作って、並列に実行することはできない。
Asynchronicity in Programming Languages
コンピュータは設計によって非同期だ。
非同期とは、メインプログラムのフローから独立して物事がおこるということだ。
現在のコンピュータでは、全てのプログラムが特定のタイムスロットで実行され、別のプログラムの実行を続けるために実行を停止する。
これは非常早いサイクルで実行され、通知することは不可能。そして、コンピュータは多くのプログラムを同時に実行すると思われがちだが、特別なマシンを除いて錯覚だ。
プログラムは内部で割り込みをし、システムのアテンションをするためにプロセッサに放出される信号である。
ここでは深く説明できないが、プログラムは通常非同期であり、アテンションを必要とするまで停止し、その間に他のことを実行することができる。プログラムがネットワークからの反応を待っているときは、リクエストが終わるまでプロセッサーを停止できない。
プログラム言語は通常同期的で、いくつかは非同期にする方法が提供されている。C, Java, C#, PHP, Go, Ruby, Swift, Pythonなどはデフォルトで同期的だ。このなかのいくつかはスレッドや新しいプロセスを生み出すことで非同期を取り扱う。
JavaScript
JSは初期では同期的で、シングルスレッドの言語だ。つまりコードは新しいスレッドを並列して作ることができない。
コードリストは連続して実行される。
const a = 1
const b = 2
const c = a * b
console.log(c)
doSomething()
しかし、JSはブラウザ内で生まれた。主な仕事として、最初にonClick, onMouseOver, onChangeといったユーザーアクションに反応する。同期プログラミングでどのようにこれを行うことができるだろうか。
それは環境次第だ。ブラウザはこれらの機能を扱うことができるAPIセットを供給することで、供給することができる。
最近では、Node.jsはファイルアクセス、ネットワークコールなどのためにこのコンセプトに拡大するためにnon-blockingI/O環境を導入した。
Callbacks
ユーザーがいつクリックするかはわからない。そのためにやることは、クリックイベントのためのイベントハンドラーを定義することだ。
このイベントハンドラーはイベントが発生した時にコールされる機能だ。
document.getElementById('button').addEventListener('click', () => {
//item clicked
})
これはいわゆるコールバックと呼ばれる。
コールバックは値として他の関数に渡される単一の関数のことだ。そしてそれはイベントが発生する時に実行される。これはJSはファーストクラス関数という、変数に割り当てられることができ、他の関数に渡すことができるからだ。(higher-order functionsと呼ばれる)
windowオブジェクト上でイベントリスナーをロードする中に、全てのクライアントコードを包むことがよくある。これはページが準備できる時のみコールバック関数を実行する。
window.addEventListener('load', () => {
//window loaded
//do what you want
})
コールバックはDOMイベントだけではなくどこでも使われる。
よくある例はタイマーを使った関数だ。
setTimeout(() => {
// runs after 2 seconds
}, 2000)
XHR requestsも、特定のイベントが発生する時にコールされるプロパティの関数に割り当てられることで、このサンプルではコールバックを受け入れる。(この場合、リクエストの状態は変化する。)
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
xhr.status === 200 ? console.log(xhr.responseText) : console.error('error')
}
}
xhr.open('GET', 'https://yoursite.com')
xhr.send()
Handling errors in callbacks
どのようにコールバックでエラーを扱うか。一般的なやり方として、Node.jsが採用したものを使用することがある。コールバック関数の最初のパラメーターは、エラーオブジェクト- エラーファーストコールバックである。
エラーがなければオブジェクトはnullになる。エラーがあると、エラーの詳細と、他の情報を持つ。
fs.readFile('/file.json', (err, data) => {
if (err !== null) {
//handle error
console.log(err)
return
}//no errors, process data
console.log(data)
})
The problem with callbacks
コールバックは簡単な場合にはすばらしい。しかし、全てのコールバックはネスト層を加える。たくさんのコールバックを持つ時、コードはすぐに複雑になり始める。
window.addEventListener('load', () => {
document.getElementById('button').addEventListener('click', () => {
setTimeout(() => {
items.forEach(item => {
//your code here
})
}, 2000)
})
})
これはたった4層のコードだが、よりたくさんのネスティングに見えてしまう。
どうすればよいか。
Alternatives to callbacks
ES6から、JSはいくつかのコールバックを使わずに非同期処理をする助けとなる機能を導入した。
- Promises (ES6)
- Async/Await (ES8)
Promises
PromisesはJSで非同期コードを扱う一つの方法で、コードにたくさんのコールバックを書かずに済む。
Introduction to promises
promisesは一般的に最終的に利用可能となる値のproxyとして定義される。
ES2015では標準化されており、ES2017ではasync関数によって置き換えられた。
async functionはビルディングブロックとしてpromises APIを使うので、新しいコードでasync関数をpromisesの代わりに使うとしても、promisesを理解する必要がある。
How promises work, in brief
promiseが呼ばれるとすぐに、pending state内でそれが始まる。つまりpromiseが自身の処理を行うことを待つ間に、caller関数は実行を続け、caller関数にフィードバックを与える。
この点で、caller関数はresolved state内のpromiseか、rejected stateのリターンを待つ。しかし、知っての通りJSは非同期だ。そのため関数はpromiseが働いている間に引き続き実行を続ける。
Which JS API use promises?
promisesは下記のAPIで使われる。
- the Battery API
- the Fetch API
- Service Workers
モダンなJSではpromiseを使っていないものもある。
Creating a promise
Promise APIはPromise constructorを公開する。それはnew Promise()を使うことで初期化できる。
let done = trueconst isItDoneYet = new Promise(
(resolve, reject) => {
if (done) {
const workDone = 'Here is the thing I built'
resolve(workDone)
} else {
const why = 'Still working on something else'
reject(why)
}
}
)
見ての通りプロミスはglobal constantのdoneをチェックし、それがtrueなら、resolved promiseを返す。そうでなければreject promiseを返す。
resolveとrejectを使用して値を返すことができ、上記の場合は文字列を返すだけだが、オブジェクトでも良い。
Consuming a promise
最後にpromiseがどのように作られるかを紹介する。
const isItDoneYet = new Promise(
//...
)const checkIfItsDone = () => {
isItDoneYet
.then((ok) => {
console.log(ok)
})
.catch((err) => {
console.error(err)
})
}
checkIfItsDone()の実行は、isItDoneYet() promiseを実行し、resolveを待ち、then コールバックを使い、もしエラーならcatchコールバッグを扱う。
Chaining promises
promiseは他のpromiseに返されることができ、promiseのチェーンが作れる。
chaining promisesのいい例は、XMLHttpRequest APIの最上層の、 Fetch APIによって与えられる。リソースを取得し、そのリソースがフェッチされたときに実行する一連の約束を待ち行列に入れるために使用することができる。
Fetch APIはpromise-based mechanismで、fetch()コールをすることはnew Promise()を使い地震のpromiseを定義するのと同じことだ。
Example of chaining promises
const status = (response) => {
if (response.status >= 200 && response.status < 300) {
return Promise.resolve(response)
}
return Promise.reject(new Error(response.statusText))
}const json = (response) => response.json()fetch('/todos.json')
.then(status)
.then(json)
.then((data) => { console.log('Request succeeded with JSON response', data) })
.catch((error) => { console.log('Request failed', error) })
この例ではtodos.jsonからTODO itemsリストを取得するためにfetch()をコールし、promisesのチェーンを作る。
fetch()を実行することは、それがたくさんのプロパティを持ち、参照するものの中でresponseを返す。
status
, HTTPステータスコードにを表すたくさんの値statusText
, リクエストが成功したらOKと返すようなステータスメッセージ
response
は json()
methodももつ。それは処理されたbodyのコンテンツでresolveしたプロミスを返し、JSONに変化する。
与えられた敷地で、これは起こることだ。最初のチェーンのpromiseは定義した関数で、status()を呼び、レスポンスステータスをチェックする。そしてレスポンスが成功しなければ(200~299でなければ)promiseを拒否する。
この操作はチェーンされたpromisesリスト全てをスキップし、promiseチェーンを引き起こす。そしてスキップして、底にあるcatch() に直接行き、エラーメッセージとともにRequest failed テキストを記録する。
この場合、処理されたJSONデータを返し、3番目のpromiseがJSONを直接受け取る。
.then((data) => {
console.log('Request succeeded with JSON response', data)
})
そして単にコンソールに記述する。
Handling errors
先ほどの例では、チェーンpromisesに添付されたcatchを持った。
promisesチェーンで何かが失敗するとき、エラーを起こすか、promiseを拒否し、コントロールはチェーンの下の最も近いcatch()ステートメントに進む。
new Promise((resolve, reject) => {
throw new Error('Error')
})
.catch((err) => { console.error(err) })// ornew Promise((resolve, reject) => {
reject('Error')
})
.catch((err) => { console.error(err) })
Cascading errors
catch()内部でエラーが起こった場合は、2番目のcatch()でそれを扱うことができる。
new Promise((resolve, reject) => {
throw new Error('Error')
})
.catch((err) => { throw new Error('Error') })
.catch((err) => { console.error(err) })
Orchestrating promises
Promise.all()
異なるpromisesを同期するなら、Promise.all()がpromiseリストの定義に役に立ち、それらすべてが解決する時に何かを実行する。
const f1 = fetch('/something.json')
const f2 = fetch('/something2.json')Promise.all([f1, f2]).then((res) => {
console.log('Array of results', res)
})
.catch((err) => {
console.error(err)
})
ES2015 destructuring assignmentシンタックスでも良い。
Promise.all([f1, f2]).then(([res1, res2]) => {
console.log('Results', res1, res2)
})
fetchの使用が制限されているわけではなく、いかなるpromiseでもよい。
Promise.race()
Promise.race()は渡された最初のpromisesが解決される時に接続されたコールバックを一度だけ実行し、最初のpromiseの結果が解決される。
const first = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'first')
})
const second = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'second')
})Promise.race([first, second]).then((result) => {
console.log(result) // second
})
Common error, Uncaught TypeError: undefined is not a promise
もしコンソールでUncaught TypeError: undefined is not a promise を取得したら、Promise()ではなくnew Promise()を代わりに使う。
【わからなかったこと】
特になし
【感想】
いよいよバックエンドで使う関数などが出てきた。一度ドットインストールで触ってみたおかげで、頭に入ってきやすい。