非同期 JavaScript の記述
学習の目的
この単元を完了すると、次のことができるようになります。
- JavaScript の重要な非同期機能を挙げる。
- setTimeout を使用して関数を非同期に呼び出す。
- コールバック関数を記述して呼び出す。
- promise ベースの関数を記述して呼び出す。
随分前のことになりますが、JavaScript エンジンについての最初の説明を思い出してください。このエンジンはシングルスレッドで、作業を行って終了し、また新しい作業が入れられると同じことを繰り返します。
ここで極めて重要な点はもちろん、スレッドがブロックされないことです。
では、次の例を見てみましょう。
<html>
<script>
alert("Does JavaScript show first?");
</script>
<body>
<p>
Does HTML show first?
</p>
</body>
</html>
この HTML ページをブラウザーに読み込むと、まず警告ポップアップが表示され、次に HTML の表示がブロックされるでしょう。ユーザーが警告を解除するまで alert()
関数が JavaScript スレッドの実行を停止するためです。結局のところ、JavaScript がブラウザーをブロックすれば、望ましいユーザーエクスペリエンスにはなりません。
幸いにも、上記の alert()
関数など、いくつかの旧来の持続的な機能を除けば、JavaScript は非同期の言語です。
非同期 JavaScript の浸透
非同期性へのジャーニーを始めるにあたって、イベントと関数をもう一度見ておきましょう。以前に次のような HTML と JavaScript を確認しました。
<!-- HTML -->
<button id="clicker">
//JavaScript
let button = document.getElementById("clicker");
button.addEventListener("click", handleClick);
この例では、handleClick
をイベントハンドラーとして、ボタンによって生じるクリックイベントに追加しました。
見てください! すでに非同期 JavaScript を記述していました。
イベントが起動された時点で生じる処理は、新しいメッセージをキューに追加することのみです。イベントがスレッドを占拠することはできません。どのイベントも起動されたらキューに入って、実行される順番を待つ必要があります。
このことを具体的に説明する 1 つの方法として、setTimeout
関数を使用します。この例では、setTimeout
を呼び出して、イベントハンドラーとタイマーを数ミリ秒のうちに渡します。タイマーの設定時間になると、イベントが起動し、イベントハンドラーがキューに追加されます。
setTimeout(function(){
console.log("This comes first");
}, 0);
console.log("This comes second");
//output in console
// "This comes second"
// "This comes first"
ここではタイマーをゼロに設定しています。けれども、「すぐさまコールされる」わけではありません。「すぐさまキューに入れられる」ということです。その一方で、コードのブロックの実行そのものを終了し、コールスタックをクリアする必要があります。setTimeout
からの関数の順番が来るのはその後です。
もう 1 つのよくある間違いが、タイマーによってイベントハンドラーの起動時点が正確に予測されると思うことですが、必ずしもそうとは限りません。イベントハンドラーは、キューの中で順番を待つ必要があります。時間を計れば、この動作を確認できます。
const timer = function(){
let start = Date.now();
setTimeout(function(){
let end = Date.now();
console.log( "Duration: " + (end - start) )
},1000);
};
timer();
// Console output when invoked several times:
// "Duration: 1007"
// "Duration: 1000"
// "Duration: 1002"
// "Duration: 1004"
この時間は 1 秒に設定されており、ほぼそのとおりに実行されます。もちろん、コールされた関数がどのくらいでキューに追加され、実行されるかにはばらつきがあります。
非同期コールの例をいくつか見てきたところで、次は一般的な非同期パターンと構成要素を見ていきましょう。
コールバックパターン
コールバックとは、別の関数に渡され、将来のある時点でその関数に呼び出される関数です。
実際のところ、このモジュールでもすでにたくさんのコールバックを見てきました。
setTimeout(callback, timer)
Element.addEventListener(event, callback)
Array.map(function(item){...})
では、このコードを bike のユースケースに適用して、コールバックがどのように実装されるか見てみましょう。自転車のギアをシフトすると、ほとんどの場合はうまくいきます。けれども、わずかながら失敗する可能性があります。これは非同期 JavaScript の理想的なシナリオです。では、ギアのシフト方法に関するデータを取り込み、その終了時に渡された関数をコールするコールバックがどのようなものか見てみましょう。
Bike.prototype.changeGearAsync = function(shiftObject, callback){
let newIndex = shiftObject.currentIndex + shiftObject.changeBy;
if (newIndex < 0 || newIndex > shiftObject.maxIndex) {
callback(new Error("There is a problem"), null);
} else {
callback(null, newIndex);
}
};
callback
引数は実際には関数です。エラーが生じた場合は、callback を呼び出して、送り返すエラーデータを指定した 1 つ目の引数を設定します。成功した場合は、エラー引数を null にして、適切なデータを返します。これで、新しいギアチェンジ関数がどのように呼び出されるのかがわかります。
Bike.prototype.changeGear = function(frontOrRear, changeBy) {
const shiftIndexName = frontOrRear + "GearIndex"
const that = this;
//contains state change for making the shift
let shiftObject = {
currentIndex: this[shiftIndexName],
maxIndex: this.transmission[frontOrRear + "GearTeeth"].length,
changeBy: changeBy
}
// invoke async function with anonymous callback
this.changeGearAsync(shiftObject, function(err, newIndex){
if (err) {
console.log("No Change");
} else {
that[shiftIndexName] = newIndex;
}
});
};
コールバックパターンは広く受け入れられ普及しましたが、いくつかの欠点があります。まず、複数のコールバックがまとめてチェーニングされると、1 つずつネストされることです。この結果、過度に複雑になり、判読性の問題が生じ、他者のコードを読み取る場合に判断がつかなくなります。この欠点をコールバック地獄といいます。次に、コールバックには (try/catch
の場合のような) 暗黙のエラー状態がありません。if
条件を使用してエラーを明示的に探すコールバックを記述するかどうかは開発者次第です。こうした問題から promise が生み出されました。
アロー関数
前の例で、この行に気付いたかもしれません。
const that = this;
これは旧来の JavaScript の遺物です。アロー関数という新しい関数構文を紹介するために、この行をを紛れ込ませました。関数が呼び出されるとどうなるか思い出してください。関数が新しい this
コンテキストにバインドされます。無名関数のクロージャーの通用範囲内の他の変数とは異なり、包含関数の this
が実際に必要になったときに、JavaScript は this
を再度バインドします。
この問題を回避するために長い間、this
を新しい変数 (慣習上、通常は self
または that
という) に代入して、コンテキスト参照をクロージャー内に維持していました。
アロー関数を使うと、this
が再バインドされず、上記のようなコーディングの離れ業を使う必要がなくなります。アロー関数構文は次のようになります。
(arg1, arg2) => {...function body...}
次のように式として記述することもできます。
const doStuff = (arg1, args) => { ...function body ...}
アロー関数を使用した場合は、that = this
の部分を削除して、changeGearsAsync
の呼び出しを次のように変更できます。
// the anonymous function is now an arrow function
this.changeGearAsync(shiftObject, (err, newIndex)=>{
if (err) {
console.log("No Change");
} else {
// we reference this instead of that
this[shiftIndexName] = newIndex;
}
});
promise について
promise は、コードが成功または失敗した時点でその判断をしやすくする方法で非同期コードを処理するライブラリとして開発されました。promise には、コールを 1 つずつチェーニングするメカニズムも組み込まれています。競合するライブラリも最終的に Promise
オブジェクトとしてブラウザーで標準化されました。では、bike
をもう一度変形させてみましょう。
Bike.prototype.changeGearAsync = function(shiftObject){
return new Promise(
(resolve, reject) => {
let newIndex = shiftObject.currentIndex + shiftObject.changeBy;
if (newIndex < 0 || newIndex > shiftObject.maxIndex) {
reject("New Index is Invalid: " + newIndex);
} else {
resolve(newIndex);
}
}
);
};
まず、変更済みの changeGearAsync
関数が渡されたデータを取り込んで、新しい Promise オブジェクトを返します。ここで 1 つの引数としてコールバック関数を渡します。この関数自体に resolve
と reject
という 2 つの関数が渡されています。
promise の実装時に、コールバック関数に必要な計算や要求などをすべて実行します。終了後、すべてうまくいけば、返すデータを指定した resolve
を呼び出します。問題が生じた場合は、関連するエラーを引数として指定した reject
を呼び出して、関数呼び出し元にその旨を伝えます。
では、ここでは this をどのように使用しているか見てみましょう。
// invoke async function that returns a promise
this.changeGearAsync(shiftObject)
.then(
(newIndex) => {
this[shiftIndexName] = newIndex;
console.log(this.calculateGearRatio());
}
)
.catch(
(err) => {console.log("Error: " + err);}
);
はるかに簡単に判断できるようになりました。changeGearAsync
が機能すれば、then
関数が呼び出され、この関数がその引数に渡されます。機能しなければ、catch
が呼び出されます。
コールバック関数自体が Promise
のインスタンスを返した場合には、事態が面白くなってきます。promise のこの 2 つの関数はまとめてチェーニングすることができます。たとえば、フロントギアとリアギアの両方を変更するとします。
Bike.prototype.changeBothGears = function(frontChange, rearChange) {
let shiftFront = {
currentIndex: this.frontGearIndex,
maxIndex: this.transmission.frontGearTeeth.length - 1,
changeBy: frontChange
};
let shiftRear = {
currentIndex: this.rearGearIndex,
maxIndex: this.transmission.rearGearTeeth.length - 1,
changeBy: rearChange
};
this.changeGearAsync(shiftFront)
.then(
(newIndex) => {
this.frontGearIndex = newIndex;
console.log(this.calculateGearRatio());
return this.changeGearAsync(shiftRear);
}
)
.then(
(newIndex) => {
this.rearGearIndex = newIndex;
console.log(this.calculateGearRatio());
}
)
.catch(
(err) => {console.log("Error: " + err);}
);
};
上記の changeBothGears
関数は、changeGearsAsync
に対する 2 つのコールをチェーニングした場合を示しています。各コールにフロントギアとリアギアのいずれかに対応するオブジェクトが指定されています。この関数を初めてコールした後、1 つ目の then
の終わりにもう一度コールします。それに別の then
を追加できます。基本的に、then
で promise が返されるたびに、別の then
を続けることができます (この連鎖的なアクションにうんざりするまで続けられます)。
async/await
最後に、非同期機能に新たに加えられた async
演算子と await
演算子にも言及しておく価値があるでしょう。この 2 つは promise 上に構築され、同期 JavaScript と極めてよく似た方法で使用できます。
Lightning Web コンポーネントと非同期 JavaScript
Lightning Web コンポーネントでは開発者が、promise ベースの非同期機能と async/await 機能の両方を利用できます。
Salesforce とのやりとり
Lightning Web コンポーネントには、非同期 JavaScript を使用する機能がいくつか実装されています。この大半は、サーバーとのやりとりに関するものです。この一例として、Lightning Web コンポーネントで Apex メソッドを命令として呼び出す方法が挙げられます。
promise ベースの API を使用して次のように呼び出します。
import { LightningElement } from 'lwc';
import findContacts from '@salesforce/apex/ContactController.findContacts';
export default class ApexImperativeMethodWithParams extends LightningElement {
searchKey = '';
contacts;
error;
handleSearch() {
findContacts({ searchKey: this.searchKey })
.then(result => {
this.contacts = result;
this.error = undefined;
})
.catch(error => {
this.error = error;
this.contacts = undefined;
});
}
}
import findContacts…
をコールする場合、これはこのコンポーネントに別のモジュールの機能を含める標準的なモジュール構文です。ここでは Apex の findContacts
メソッドを同じ名前の JS 関数として表示します。
handleSearch()
関数で呼び出すと、Apex メソッドのパラメーターがリテラルオブジェクトとして渡され、then と catch
関数の promise ベースの同様の構文が表示されます。
リソース
- JavaScript のアロー関数式
- JavaScript の Promises
- JavaScript Async keyword (JavaScript の async キーワード)
- JavaScript Await keyword (JavaScript の await キーワード)