Skip to main content

非同期 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 つの引数としてコールバック関数を渡します。この関数自体に resolvereject という 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;
});
}
}
メモ

このモジュールの範疇を超えている点がいくつかあります。その代表例が、ES6 での JavaScript モジュールの使用法です。こうした機能に関心のある方は、このトレイルの次の「Modern JavaScript Development (JavaScript の最新機能)」というモジュールを続けて受講することを検討してください。ただし、その前にこのコードについて少し説明しておきます。 

import findContacts… をコールする場合、これはこのコンポーネントに別のモジュールの機能を含める標準的なモジュール構文です。ここでは Apex の findContacts メソッドを同じ名前の JS 関数として表示します。 

handleSearch() 関数で呼び出すと、Apex メソッドのパラメーターがリテラルオブジェクトとして渡され、then と catch 関数の promise ベースの同様の構文が表示されます。 

リソース

無料で学習を続けましょう!
続けるにはアカウントにサインアップしてください。
サインアップすると次のような機能が利用できるようになります。
  • 各自のキャリア目標に合わせてパーソナライズされたおすすめが表示される
  • ハンズオン Challenge やテストでスキルを練習できる
  • 進捗状況を追跡して上司と共有できる
  • メンターやキャリアチャンスと繋がることができる