2023.02.06

【LWC基礎】非同期処理とPromiseの関係

Just a moment... (31573)

はじめに

Lightning Web ComponentsでApexを呼び出すときに経験の浅いエンジニアがうっかりハマってしまうことがあります。
それはApex呼び出しが非同期になるということです。

JavaScriptでコーディングした処理の順序はあっているのに何故かエラーが出てしまい、Apex側で値がセットできていないのでは?と全然違う場所までエラーの原因を探しに行く方もいるかもしれません。

今回は非同期処理を同期処理として実行する方法と、その過程で登場するPromiseオブジェクトについて理解を深めていきたいと思います。

非同期処理ってどんなもの?

非同期処理とは、処理の完了を待たずに次の処理を実行する方法​です。
同期処理とは、処理の完了を待って次の処理を実行する方法です。

実際の実行結果を見比べてみましょう。
以下のサンプルコードは実行ボタンをクリックしたときに非同期処理が含まれると、実行順序がどのように変化するかコンソールログで確認する一例になります。
<template>
    <lightning-button onclick={handleExecution} label="実行"></lightning-button>
</template>
component.html
import { LightningElement } from 'lwc';
export default class Component1 extends LightningElement {
        handleExecution() {
                console.log( '*** 同期処理 ***' );
                console.log( '・処理1' );
                console.log( '・処理2' );
                console.log( '・処理3' );

                console.log( '*** 非同期処理 ***' );
                console.log( '・処理1' );
                // setTimeout('コールバック関数', 'タイムアウト時間(ms)')
                setTimeout( function(){ console.log( '・処理2' ); }, 0 );
                console.log( '・処理3' );
        }
}
component1.js

コードの途中に非同期処理のsetTimeoutを入れています。
結果は非同期の「処理2」が実行される前に、後続の「処理3」が先に実行されました。
非同期の処理の完了を待たずに次の処理を実行していることがわかります。

コールバック

非同期処理同期処理として実行する場合はコールバックを利用します。
コールバックとは、実行する関数の引数に関数を渡し、必要な処理が完了した後に渡した関数を実行する方法です。
非同期処理の引数に関数を渡し、非同期処理の完了後に渡した関数を実行するように実装することで、同期処理を作ることができます。

以下サンプルコードはコールバック関数によって、非同期処理を同期処理と同様の処理順序にしています。
import { LightningElement } from 'lwc';
export default class Component2 extends LightningElement {
        handleExecution() {
                console.log( '*** 非同期⇒同期処理 ***' );
                console.log( '・処理1' );
                // setTimeout('コールバック関数', 'タイムアウト時間(ms)')
                setTimeout( function(){
                        console.log( '・処理2' );
                        console.log( '・処理3' );
                        setTimeout( function(){
                                console.log( '・処理4' );
                                console.log( '・処理5' );
                        }, 0 );
                }, 0 );
        }
}
component2.js

非同期処理のsetTimeoutが実行されるごとに、後続処理を全てまとめてコールバック関数に入れて渡しています。
これにより処理順序が逆転することなく、「処理1」~「処理5」までの処理が順次実行されていることがわかります。

コールバック関数の処理順序のイメージがしやすいように、以下にもう1つサンプルコードを記載します。
import { LightningElement } from 'lwc';
export default class Component3 extends LightningElement {
        handleExecution() {
                console.log( '*** 非同期処理サンプル ***' );
                this.asyncSample( function(){
                        console.log( 'コールバック関数' );
                }, '非同期処理の実行' );
        }

        asyncSample( callback, param ) {
                // メイン処理
                console.log( param );
                // コールバック関数を実行
                callback();
        }
}
component3.js

呼び出した非同期処理(仮)のasyncSample内でコールバック関数が全ての処理の後に実行されています。
コールバック関数として渡した処理は非同期処理の後に実行されるイメージが付いたと思います。

Promiseオブジェクト

Promiseオブジェクト非同期処理の状態を監視し、処理完了時に実行結果を返すことができます。
これにより非同期処理を同期処理として簡単に実装することができます。

そして、Lightning Web ComponentsからApexを呼び出した場合の戻り値はPromiseオブジェクトが返ってきま
非同期処理のApex実行結果が返るのを待機し、その結果をもとに後続処理を行う実装方法を覚えましょう。

以下サンプルコードはPromiseを利用して非同期処理の結果を判断する処理になります。
import { LightningElement } from 'lwc';
export default class Component4 extends LightningElement {
        handleExecution() {
                console.log( '*** Promise1 ***' );
                const promise = new Promise( function( resolve, reject ) {
                        console.log( '非同期処理:実行' );
                        setTimeout( function(){
                                console.log( '非同期処理:完了' );
                                resolve(); // 成功の状態
                        }, 0 );
                } );
                promise
                .then( function(){ console.log( '正常終了' ); } )
                .catch( function(){ console.log( '異常終了' ); } );
        }
}
component4.js
import { LightningElement } from 'lwc';
export default class Component5 extends LightningElement {
        handleExecution() {
                console.log( '*** Promise2 ***' );
                const promise = new Promise( function( resolve, reject ) {
                        console.log( '非同期処理:実行' );
                        setTimeout( function(){
                                console.log( '非同期処理:完了' );
                                reject(); // 失敗の状態
                        }, 0 );
                } );
                promise
                .then( function(){ console.log( '正常終了' ); } )
                .catch( function(){ console.log( '異常終了' ); } );
        }
}
component5.js

Promiseオブジェクトは引数にresolveとrejectのコールバック関数を持ちます。
中の処理で成功(resolve)失敗(reject)の状態に変化させ、処理完了として結果を戻します。
戻した結果を使って、次の2つの方法で後続処理を実行することができます。

thenメソッド

Promiseオブジェクトが持つメソッドで、処理結果が成功(resolve)のときに実行されます。
後続処理は基本的にこのメソッドの内部(コールバック関数)に記載していきます。
このメソッドが実行された場合、catchメソッドは実行されません。

Apexのメソッドを呼び出した戻り値の場合、処理が正常終了したときに実行されます。
引数にはApexメソッドの戻り値が設定されます。

catchメソッド

Promiseオブジェクトが持つメソッドで、処理結果が失敗(reject)のときに実行されます。
このメソッドの内部(コールバック関数)にはApexのtry-catchと同様に例外処理を記載します。

Apexのメソッドを呼び出した戻り値の場合、処理が異常終了したときに実行されます。
引数にはApexメソッドの例外が設定されます。

async / await

Promiseオブジェクトをさらに簡潔にした構文になります。
同じく非同期処理を同期処理として実装することができます。

async

asyncは関数宣言の前に付けることで、非同期関数を定義することができます。
async関数は戻り値がPromiseオブジェクトを返し、関数内でreturn(正常終了)した場合はresolve(成功)、関数内でthrow(異常終了)した場合はreject(失敗)の結果を返します。

await

awaitasync関数内で使用することができます。
関数呼び出しの前に付けることで、呼び出された関数からPromiseオブジェクトの結果が返るまで処理を待機します。
これを使って非同期関数を呼び出すことで、非同期処理を同期処理として実行することができます。
次のサンプルコードは同じ処理内容をPromiseオブジェクトのthen-catchメソッドで処理するケースasync/await構文で処理するケースで表現しています。
import { LightningElement } from 'lwc';
import apexMethod from '@salesforce/apex/ApexClass.apexMethod'
export default class Component6 extends LightningElement {
        handleExecution() {
                console.log( '*** Apexメソッド呼び出し ***' );
                apexMethod()
                .then( ( result ) => { console.log( '正常終了' ); } ) // アロー関数化
                .catch( ( error ) => { console.log( '異常終了' ); } ); // アロー関数化
        }
}
component6
import { LightningElement } from 'lwc';
import apexMethod from '@salesforce/apex/ApexClass.apexMethod'
export default class Component7 extends LightningElement {
        // async関数化
        async handleExecution() {
                console.log( '*** Apexメソッド呼び出し ***' );
                try {
                        // awaitで後続処理の待機
                        const result = await apexMethod();
                        console.log( '正常終了' );
                } catch (error) {
                        console.log( '異常終了' );
                }
        }
}
component7
2つのコードを比べてみて、いかがでしょうか。
async/await構文は馴染みのあるApexの書き方に近い感じするのではないでしょうか。

この2つのコーディングには大きな特徴があります。
1つの関数内に複数回のApex呼び出し処理(非同期処理)を実装するケースを仮定してみてください。
then-catchメソッドだとコールバック関数に後続処理を入れ込んでいくのでネストが深くなることが予想できます。
ところがasync/await構文だとネストが発生しないため、変わらず簡潔なコードのままだと気付きます。

おわりに

普段あまり意識せず使っている機能をふと調べてみると新しい発見があります。
仕組みを知るとさらに理解が深まるので、皆さんも気になったことは時間のあるときで良いので忘れずに調べてみるようにしましょう。
39 件
     
  • banner
  • banner

関連する記事