こんにちはWebエンジニアのmasakichiです。
JavaScriptの非同期を理解するにはMDN Web Docsを読むべき。翻訳しといたよ 2つ目の記事です。
全部で5記事あります。
- Introducing asynchronous JavaScript
- Cooperative asynchronous JavaScript: Timeouts and intervals ←いまここ
- Graceful asynchronous programming with Promises
- Making asynchronous programming easier with async and await
- Choosing the right approach
翻訳箇所について
こちらのページの日本語未翻訳記事です。
なぜ翻訳したか
JavaScriptの非同期処理。特にPromiseというやつが、ぼんやりわかっているけど完全には理解していない状態がずっと続いていました。
そんな中、MDN Web Docsの解説がすごくわかりやすく一気に理解できました。
しかし、これらの記事は日本語に翻訳されていないという問題が。。。
ぜひ非同期処理で悩める同志にも読んでほしい。という想いで翻訳作業をしてみました。
留意点
筆者は英語がそこまで得意というわけではありません。DeepLの力を借りて、翻訳していますので、日本語訳が不自然なところや一部、情報を省略・意訳しています。あらかじめご了承ください。
ライセンスは下記です。
This Article by Mozilla Contributors is licensed under CC-BY-SA 2.5.
協調的な非同期JavaScript:タイムアウトとインターバル
このチュートリアルでは、設定した時間が経過した後、あるいは一定の間隔(例えば1秒間に何回)でコードを非同期に実行するためにJavaScriptが利用できる従来の方法を取り上げ、それらが何のために有用であるかを議論し、固有の問題を考察しています。
前提条件
基本的なコンピュータリテラシー / JavaScriptの基本をそれなりに理解していること。
目的
非同期ループとインターバルについて理解し、何に役立つかを理解する。
イントロダクション
以前からWebプラットフォームでは、JavaScriptプログラマーに対して、ある時間間隔が経過した後に非同期でコードを実行したり、停止を指示するまで非同期でコードブロックを繰り返し実行したりする機能が数多く提供されています。
これらの機能は
setTimeout()
:
指定された時間経過後に、指定されたコードブロックを1回実行する。
setInterval()
:
指定されたコードのブロックを、呼び出しのたびに一定の時間遅らせて繰り返し実行する。
requestAnimationFrame()
:
setInterval()の現代版。ブラウザが次にディスプレイを再描画する前に指定したコードブロックを実行し、実行中の環境に関係なく適切なフレームレートでアニメーションを実行できるようにします。
これらの関数で設定された非同期コードは、(指定されたタイマーが経過した後)メインスレッドで実行されます。
setTimeout()の呼び出しが実行される前や、setInterval()の繰り返しの間に、他のコードを実行できる(そしてしばしば実行する)ことを知っておくことが重要です。非同期コードはメインスレッドが利用可能になった後にのみ実行されるため、これらの操作がどれほどプロセッサ集約的であるかによって、それらは非同期コードをさらに遅らせることができます。(なぜなら、非同期コードはメインスレッドが使用可能になった後にのみ実行されるからです(言い換えれば、スタックが空になったとき)。この件に関しては、この記事を読み進めるにつれて詳しく知ることができます。
いずれにせよ、これらの関数は、Webサイトやアプリケーションで一定のアニメーションやその他のバックグラウンド処理を実行するために使用されます。以下のセクションでは、これらの関数の使用方法について説明します。
setTimeout()
先に述べたように、setTimeout()は指定された時間が経過した後、特定のコードブロックを一回実行します。これは以下のパラメータをとります。
- 実行する関数、または他の場所で定義された関数への参照。
- コードを実行する前に待機する時間間隔をミリ秒単位で表した数値(1000ミリ秒は1秒に相当)。0を指定すると(あるいは値を省略すると)、関数はできるだけ早く実行されます。(なぜ「すぐに」ではなく「できるだけ早く」実行するのかについては、以下の注釈を参照してください)。なぜこのようなことをしたいのかについては、後で詳しく説明します。
- ゼロまたはそれ以上の値で、関数が実行されるときに渡したいパラメータを表します。
注意:指定された時間(または遅延時間)は、実行までの保証時間ではなく、実行までの最短時間です。これらの関数に渡すコールバックは、メインスレッド上のスタックが空になるまで実行できません。
その結果、setTimeout(fn, 0)のようなコードは、スタックが空になるとすぐに実行されるのではなく、すぐに実行されます。setTimeout(fn, 0)のようなコードを実行し、その直後に1から100億までカウントするループを実行すると、コールバックは数秒後に実行されることになります。
次の例では、ブラウザは匿名関数を実行する前に2秒間待機し、その後アラートメッセージを表示します。
let myGreeting = setTimeout(() => {
alert('Hello, Mr. Universe!');
}, 2000);
指定する関数は、匿名である必要はありません。関数に名前をつけて、どこか別の場所で定義して、setTimeout()に関数の参照を渡すことも可能です。次の2つのバージョンのコードスニペットは、最初のコードと同等です。
// With a named function
let myGreeting = setTimeout(function sayHi() {
alert('Hello, Mr. Universe!');
}, 2000);
// With a function defined separately
function sayHi() {
alert('Hello Mr. Universe!');
}
let myGreeting = setTimeout(sayHi, 2000);
例えば、setTimeout()からの呼び出しされり、とあるイベントに呼び出されたりと、複数のことに対応する必要がある関数の場合には役立ちます。また、setTimeout()のコールバックが数行以上のコードである場合、コードを整理整頓するのにも特に役立ちます。
setTimeout() は、タイムアウトを停止させたいときなど、後で参照するために使用できる識別子の値を返します。その方法については、タイムアウトの解除 (後述) を参照ください。
setTimeout() 関数へのパラメータの渡し方
setTimeout()の内部で実行される関数に渡したいパラメータは、リストの最後に追加パラメータとして渡す必要があります。
例えば、前の関数をリファクタリングして、渡された人の名前が何であれ、こんにちはと言うようにすることができます。
function sayHi(who) {
alert(`Hello ${who}!`);
}
ここで、setTimeout()の呼び出しに、3番目のパラメータとして人の名前を渡すことができます。
let myGreeting = setTimeout(sayHi, 2000, 'Mr. Universe');
タイムアウトのクリア
最後に、タイムアウトを作成した場合、指定した時間が経過する前に、clearTimeout()を呼び出し、パラメータとしてsetTimeout()呼び出しの識別子を渡すことで、タイムアウトをキャンセルすることができます。つまり、上記のタイムアウトをキャンセルするには、次のようにします。
clearTimeout(myGreeting);
setInterval()
setTimeout() は、一定時間後に一度だけコードを実行する必要がある場合に完璧に機能します。しかし、何度も何度もコードを実行する必要がある場合、たとえばアニメーションの場合はどうなるのでしょうか。
そこで登場するのが、setInterval()です。これは setTimeout() と非常によく似た方法で動作しますが、 最初のパラメータとして渡した関数が一度だけでなく、 二番目のパラメータで指定したミリ秒以上の間隔で繰り返し実行される点が異なります。setInterval()の呼び出しの後続のパラメータとして、実行される関数が必要とする任意のパラメータを渡すことも可能です。
例を見てみましょう。次の関数は、新しい Date() オブジェクトを作成し、toLocaleTimeString() を使用してそこから時間文字列を抽出し、UI に表示します。そして、setInterval() を使って1秒に1回この関数を実行し、1秒に1回更新されるデジタル時計の効果を作り出しています。
function displayTime() {
let date = new Date();
let time = date.toLocaleTimeString();
document.getElementById('demo').textContent = time;
}
const createClock = setInterval(displayTime, 1000);
setTimeout()と同様に、setInterval()は、後で間隔を空ける必要があるときに使用できる識別値を返します。
インターバルのクリア
setInterval() は、あなたが何かしない限り、永遠にタスクを実行し続けます。しかし時にはタスクを止めたいこともあるでしょう。そうしないと、ブラウザがタスクのそれ以上のバージョンを完了できないとき、またはタスクによって処理されるアニメーションが終了したときに、エラーを受け取ることになるかもしれません。停止方法は setTimeout() と同じです。setInterval() の呼び出しによって返された識別子を clearInterval() 関数に渡すことで行えます。
const myInterval = setInterval(myFunction, 2000);
clearInterval(myInterval);
setTimeout() と setInterval() の注意点
setTimeout()とsetInterval()を扱う際には、いくつか注意すべき点があります。今一度、これらを確認しておきましょう。
再帰的タイムアウト
setTimeout()の使い方はもう一つあります。setInterval()を使う代わりに、再帰的に呼び出して同じコードを繰り返し実行させることができます。
以下の例では、再帰的に setTimeout() を使用して、渡された関数を100ミリ秒ごとに実行しています。
let i = 1;
setTimeout(function run() {
console.log(i);
i++;
setTimeout(run, 100);
}, 100);
上の例と次の例を比べてみてください。これは、setInterval()を使って同じ効果を得ています。
let i = 1;
setInterval(function run() {
console.log(i);
i++;
}, 100);
再帰的なsetTimeout()とsetInterval()の違い
上記の2つのコードの違いは、微妙なところです。
- 再帰的なsetTimeout()は、コードの実行完了から次の呼び出しまでに与えられた遅延を保証します。つまり次の実行は、コードの実行が終了してからカウントを開始するため、コードの実行にかかった時間は除かれます。この例では、100ミリ秒は実行コードが終了してから次の実行が呼び出されるまでの遅延時間になります。
- setInterval()を使用した例では、多少異なる点があります。繰り返す間隔には、実行したコードの実行にかかる時間が含まれてしまいます。例えば、コードの実行に 40 ミリ秒かかるとすると、間隔は 60 ミリ秒にしかなりません。
- setTimeout()を再帰的に使用する場合、各反復処理では、次の反復処理を実行する前に異なる遅延を計算することができます。つまり、第2引数の値によって、コードを再実行するまでの待機時間をミリ秒単位で指定できます。
コードの実行に割り当てた時間間隔よりも長い時間がかかる可能性がある場合は、再帰的な setTimeout() を使用したほうがよいでしょう。この方法では、コードの実行にかかる時間にかかわらず、実行間隔を一定に保ち、エラーが発生することはありません。
timeoutの即時実行
setTimeout()の値として0を使用すると、指定されたコールバック関数をできるだけ早く実行しますが、メインコードスレッドが実行された後にのみ実行されます。
例えば、以下のコードでは、最初のアラートでOKをクリックするとすぐに「Hello」を含むアラートが出力され、次に「World」を含むアラートが出力されます。
setTimeout(function() {
alert('World');
}, 0);
alert('Hello');
これは、メインスレッドの実行がすべて終了したらすぐに実行するコードブロックを設定したい場合に便利です。非同期イベントループに置くと、その直後に実行されます。
requestAnimationFrame()
requestAnimationFrame()は、ブラウザ上でアニメーションを効率的に実行するために作られた、特殊なエンキュー関数です。これは、ブラウザが次にディスプレイを再描画する前に指定されたコードのブロックを実行し、デバイスのディスプレイフレームレートと組み合わせて実行できるようにします。
setInterval()のような従来の非同期関数では、デバイスに最適化されたフレームレートで実行されず、場合によってはフレームが落ちるなどの問題が指摘されていたため、この関数が作成されました。また、アニメーションに適した最適化も欠けており、タブがアクティブでない場合やアニメーションがページの外にスクロールされた場合などに実行を停止するなどの問題がありました。
このメソッドは、再描画の前に呼び出されるコールバックを引数として受け取ります。一般的なパターンとしては下記のように使用されます。
function draw() {
// Drawing code goes here
requestAnimationFrame(draw);
}
draw();
上記では、まずアニメーションの更新(スプライトの移動、スコアの更新、データのリフレッシュなど)を行う関数を定義しています。そして、この関数を呼び出して、処理を開始します。関数ブロックの最後に、関数自体をパラメータとして渡した requestAnimationFrame()
を呼び出し、次のディスプレイの再描画時に再び関数を呼び出すようにブラウザに指示します。これは、requestAnimationFrame()
を再帰的に呼び出しているため、継続的に実行されます。
アニメーションの再生速度は?
アニメーションの滑らかさは、アニメーションのフレームレートに直接依存し、それはfps(frames per second)単位で測定されます。この数値が高ければ高いほど、アニメーションの見た目はある程度滑らかになります。
ほとんどの画面のリフレッシュレートは60Hzなので、Webブラウザで作業する場合、最も速いフレームレートは60フレーム/秒(FPS)を目指すことができます。しかし、フレーム数が増えると処理量が増えるので、しばしば吃音や音飛びが発生することがあります。
リフレッシュレート60Hzのモニタで60FPSを実現する場合、各フレームをレンダリングするためのアニメーションコードを実行する時間は約16.7ミリ秒(1000 / 60)です。これは、アニメーションのループを通過するたびに実行しようとするコードの量に注意する必要があることを心掛けるべきということです。
requestAnimationFrame()
は常に、この魔法の 60 FPS 値にできるだけ近づけようとします。時にはそれが不可能なこともあります。本当に複雑なアニメーションで、遅いコンピュータで実行している場合は、フレームレートが低くなります。どのような場合でも、requestAnimationFrame()
は常に可能な限り最善を尽くします。
requestAnimationFrame() と setInterval() や setTimeout() はどう違うか?
ここで、requestAnimationFrame()
メソッドが、先ほど使用した他のメソッドとどのように違うのか、少し詳しく説明します。先ほどのコードを見てみましょう
function draw() {
// Drawing code goes here
requestAnimationFrame(draw);
}
draw();
次に、setInterval()を使って同じことをする方法を見てみましょう。
function draw() {
// Drawing code goes here
}
setInterval(draw, 17);
先ほど取り上げたように、requestAnimationFrame()
には時間間隔を指定しません。ただ、現在の状況で可能な限り速く、スムーズに実行されるだけです。また、ブラウザは、何らかの理由でアニメーションが画面の外に出てしまった場合などにも、無駄な時間をかけて実行することはありません。
一方、setInterval()
では、間隔を指定する必要があります。1000ミリ秒 / 60Hzの計算式で17という値を導き出し、それを切り上げました。切り上げるのは良いアイデアです。切り下げると、ブラウザは60FPSより速い速度でアニメーションを実行しようとするかもしれませんし、どのみちアニメーションの滑らかさには違いがありません。先にも述べたように、60Hzは標準的なリフレッシュレートです。
タイムスタンプを含める
requestAnimationFrame()
関数に渡される実際のコールバックには、パラメータとして、requestAnimationFrame()
の実行開始からの時間を表すタイムスタンプ値も与えることができます。
これは、デバイスの速さや遅さに関係なく、特定の時間に一定のペースで処理を実行できるようにするために便利です。一般的なパターンは次のようなものです。
let startTime = null;
function draw(timestamp) {
if (!startTime) {
startTime = timestamp;
}
currentTime = timestamp - startTime;
// Do something based on current time
requestAnimationFrame(draw);
}
draw();
ブラウザサポート
requestAnimationFrame()
は setInterval()
/ setTimeout()
よりも最近のブラウザでサポートされています。Internet Explorer 10 以降で利用可能です。
そのため、古いバージョンのIEに対応する必要がない限り、requestAnimationFrame()
を使わない理由はほとんどないでしょう。
requestAnimationFrame()の呼び出しをクリアする
requestAnimationFrame()
の呼び出しをクリアするには、cancelAnimationFrame()
メソッドを呼び出すことで可能です。(関数名は "set..." メソッドのように "clear" ではなく、 "cancel" で始まることに注意してください)。
変数rAFに格納した、requestAnimationFrame()
の値を渡すだけで、キャンセルすることができます。
cancelAnimationFrame(rAF);