進行状況の追跡を始めよう
Trailhead のホーム
Trailhead のホーム

非同期 JavaScript の記述

学習の目的

この単元を完了すると、次のことができるようになります。

  • JavaScript の重要な非同期機能を挙げる。
  • setTimeout を使用して関数を非同期に呼び出す。
  • コールバック関数を記述して呼び出す。
  • promise ベースの関数を記述して呼び出す。
  • Aura コンポーネントの非同期機能について説明する。

随分前のことになりますが、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...}

アロー関数を使用した場合は、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 機能の両方を利用できます。ここでの唯一のアドバイスは、開発者が Internet Explorer 11 でユーザ向けの機能を作成する場合、そのブラウザに async/await が実装されていないことです。心配いりません。コードが機能します。どのような場合でも、IE11 で async/await を使用して何かを実行すると、LWC が自動的にポリフィルを使用するため、構文が正しく機能します。こうした理由により、async/await を頻繁に使用する場合は、IE11 上のパフォーマンスがやや低下することがあります。 

Salesforce とのやりとり

Lightning Web コンポーネントには、非同期 JavaScript を使用する機能がいくつか実装されています。この大半は、サーバとのやりとりに関するものです。この一例として、Lightning Web コンポーネントで Apex メソッドを命令として呼び出す方法が挙げられます。 

次の Apex クラスとメソッドを考えてみます。 

public with sharing class ContactController {
    @AuraEnabled(cacheable=true)
    public static List<Contact> findContacts(String searchKey) {
        if (String.isBlank(searchKey)) {
            return new List<Contact>();
        }
    String key = '%' + searchKey + '%';
    return [SELECT Id, Name, Title, Phone, Email, Picture__c FROM Contact WHERE Name LIKE :key AND Picture__c != null LIMIT 10];
    }
...

Lightning Web コンポーネントは promise ベースの API を使用してこのメソッドを表示します。次のとおり呼び出します。 

import { LightningElement, track } from 'lwc';
import findContacts from '@salesforce/apex/ContactController.findContacts';
export default class ApexImperativeMethodWithParams extends LightningElement {
    @track searchKey = '';
    @track contacts;
    @track 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 ベースの同様の構文が表示されます。 

リソース

JavaScript のアロー関数式

JavaScript の Promises

JavaScript Async keyword (JavaScript の async キーワード)

JavaScript Await keyword (JavaScript の await キーワード)