LoginSignup
1
1

More than 1 year has passed since last update.

【JavaScript】Promiseがよくわからないから改造して遊んでみた 〜「任意の状態が完了するまで待つ」という処理をつくる方法〜

Last updated at Posted at 2022-01-08

最終的に完成したコード

結果から言え、と怒られそうな日記のような記事になってしまったので、
とりあえず「任意の状態が完了するまで待つ」という処理の書き方だけ頭に持ってきます。

※ このコードはあくまでPromiseの理解をする過程で副産物的に生まれたコードであり、記事の本質はこのコードが生まれる過程で理解した内容です。
※ このような変則的な書き方を利用すると、Promiseの本来の使い方(then/catch)を普通に理解できている人たちを混乱させてしまうので、ご利用は慎重にお願いします。

function generateControllablePromise( resolveFunctionName, rejectFunctionName ){
    let resolveFunction, rejectFunction;
    const promise = new Promise((res,rej)=>{ resolveFunction = res; rejectFunction = rej; });
    promise[resolveFunctionName] = resolveFunction;
    promise[rejectFunctionName] = rejectFunction;
    return promise;
}

const 状態監視用の変数 = generateControllablePromise( '状態を完了させるメソッドの名前', '状態をエラーにするメソッドの名前' );

使い方:
1. 状態が完了するまで待たせたい部分では await 状態監視用の変数; というコードを挿入しておく。
2. 任意のタイミングで状態監視用の変数.状態を完了させるメソッドの名前(); を実行する。
3. ↑の関数が実行されると、await 状態監視用の変数; で待たせていた処理が発動する。

例:

const loading = generateControllablePromise( 'complete', 'error' );

async function anyProcess(){
    await loading;
    // これ以降の処理の実行は、loadingが完了するまで待ってもらうことができる。
    console.log('ロードが完了しました!');
}
anyProcess();

if( isLoaded ){
    // 状態を完了させるメソッドを実行することで、待たせていた処理を発動させることができる。
    loading.complete();
}

あらすじ

いままで非同期処理をつかうときはPromiseやthen/catchを使う書き方を避けて、
async/awaitだったりtry/catchしか使ってこなかったので、
new Promise( ...してるコードなんかを見ると、死んだ魚の目になっていました。

しかし今回、作っている作品の中で
GoogleAPIの読み込み状況を管理しなきゃいけない状況があり
どうやってやればいいのかの調べていたら、
ひときわ「なんじゃこのPromiseの使い方は!!?」と思うような書き方と出会いました。
その書き方を読んでみたり、いじくってみたりして、理解できたことをメモがてらまとめようと思います。

※ async/awaitの使い方も理解できてない人には、私が昔書いた記事をオススメしときます。
【JavaScript】非同期処理とasync/await ~難しいこと抜きで、まず使いはじめるための知識~ ( ´ε` )💻

注意
この記事は、Promiseの説明サイトなどを読むのを避けてきた程度の知能の猿人が、
実際にPromiseを使うコードをいじってみることで、やっとPromiseのなんたるかを理解した
という話をまとめているだけの記事です。
Promiseの説明を読んで普通に理解できる人は
この記事を読むと、知能レベルの低すぎる文章によってストレスを感じる恐れがあります。

きっかけとなったPromiseの書き方

こちらの記事に書かれていたコードです。
How to use async functions with gapi, the Google Drive client, and the file picker
和訳:gapi、Googleドライブクライアント、ファイルピッカーで非同期関数を使用する方法

※ 「gapi」とは、GoogleAPIを<script>タグで読み込んだとき、グローバルに定義してくれる変数の名前です。(GoogleAPIを実行するために必要なものを詰め込んだオブジェクト)

<script>
    const gapiPromise = new Promise((res, rej) => {
        window.gapiLoaded = res;
        window.gapiLoadError = rej;
    });
</script>
<script async defer src="https://apis.google.com/js/api.js"
    onload="window.gapiLoaded()"
    onerror="window.gapiLoadError(event)"
>

この構造を使用すると、await gapiPromise;するだけで、gapiが読み込まれ、エラーを処理できる場所で適切にエラーが提供され、init関数を中心にアプリを再構築する必要がなくなります。

最初目にしたときは頭を抱えましたが、
await gapiPromise;をすれば gapi というオブジェクトの生成を待つことができる」
というところから、onload="window.gapiLoaded()"という部分にPromiseの真髄が詰まっているような気がしました。

<script>のonload属性は、おそらく説明を読むまでもなく「外部スクリプトの読み込みが終わったときに実行される処理」だとおもうので、
window.gapiLoaded() という部分が理解がミソなのだろうと予測しました。

window.gapiLoaded()の理解

window.gapiLoaded()は、gapiPromiseの定義をする際に定義されている関数です。
これを叩くことで gapiPromise が完了状態になるということは、
gapiPromiseは、生成された時点では未完了状態、
gapiLoaded()を実行された時点で完了状態になる、ということです。

(いうまでもなく、きっとgapiLoadError()も同様の処理なのでしょう。)

改造してみる

すこしずつ改造

window.○○○ としているのは、おそらく gapiLoaded() を
new Promise のスコープの外に出してあげるための記述だと思うので、↓のように書き方を変えても動くハズ、と考えました。→ 無事動きました。

let gapiLoaded, gapiLoadError;
const gapiPromise = new Promise((res, rej) => {
    gapiLoaded = res;
    gapiLoadError = rej;
});

ということは、↑のコードは、下記のような動きをしているという事です。
1. gapiPromiseという変数には、状態が完了しているかどうかを確認できるPromiseインスタンスが代入される。
1. gapiLoadedという変数には、そのPromiseインスタンスを完了状態に持っていくための関数が代入される。
1. gapiLoadErrorという変数には、そのPromiseインスタンスをエラー状態に持っていくための関数が代入される。

promiseとかresとかrejとか、見慣れない単語で表現されると理解しづらいので、
日本語に翻訳してみました。

let 状態を完了させるための関数, 状態をエラーにするための関数;
const 状態を監視するための変数 = new Promise((res, rej) => {
    状態を完了させるための関数 = res;
    状態をエラーにするための関数 = rej;
});

結果的に、new Promise( 関数 ) というコードを書くと、
引数に渡した関数の第一引数に 状態を完了させるための関数 が渡され、
引数に渡した関数の第二引数に 状態をエラーにするための関数 が渡される、ということがわかりました。
(多分どのサイトでもちゃんとそのように説明してあるのだけど、自分はこうやって紐解いてみるまでよくわからなかった。)

逆に言うと
状態を監視するための変数 の状態を完了させたいなら 状態を完了させるための関数 を実行すればいいし、
状態を監視するための変数 の状態をエラーにしたいなら 状態をエラーにするための関数 を実行すればいいということ。

Promiseがわかってきた感じがしました。

がっつり改造

ということは、本来Promiseを完了させたりエラーにさせたりの状態変化は、
new Promiseの処理の中で行うのが正しい形なんでしょう。
(そのうえで .then.catch で処理を繋いでいく。)

しかし、async/awaitのようにネストしないスタイルでの非同期処理に慣れている自分は、
それはとても使いづらいと感じました。

なので、ネストせずにPromiseの状態を動かせるように改造して遊んでみました。

応用1.
状態監視用の変数 と 完了にする関数 と エラーにする関数
これら3つをまとめて生成してくれる関数をつくってみた。

function generatePromiseSet(){
    let onReady, onError;
    const promise = new Promise((res,rej)=>{ onReady = res; onError = rej; });
    return { 状態監視用の変数: promise, 完了する関数: onReady, エラーにする関数: onError };
}

使ってみる
※ 「分割代入」の仕組みを利用しています。

const { 状態監視用の変数, 完了する関数, エラーにする関数 } = generatePromiseSet();

async function テスト(){
    console.log('開始');
    await 状態監視用の変数;
    console.log('完了!');
}

テスト();

結果:
'開始'が出力されたあと、完了する関数()を実行するまで'完了!'は出力されなかった。
完了する関数()を実行すると、'完了!'が出力された。)

ぷち感動です。

ただ、グローバルに3つも状態監視に関する変数を定義するのは、なんだか汚い気がしたので、
オブジェクト指向的なかんじで、状態監視用の変数にすべてまとめることができないかも、試してみました。

応用2.
自分自身を完了・エラー状態に変更できる状態監視用の変数を生成してみた。

function generateControllablePromise(){
    let onReady, onError;
    const promise = new Promise((res,rej)=>{ onReady = res; onError = rej; });
    promise.完了する = onReady;
    promise.エラーにする = onError;
    return promise;
}

使ってみる

const 状態監視用の変数 = generateControllablePromise();

async function テスト(){
    console.log('開始');
    await 状態監視用の変数;
    console.log('完了!');
}

テスト();

結果:
'開始'が出力されたあと、状態監視用の変数.完了する()を実行するまで'完了!'は出力されなかった。
状態監視用の変数.完了する()を実行すると、'完了!'が出力された。)

うん!いい感じです。

魔改造

どうせなら、使い回しが効くように、
状態を完了にもっていく関数の名前も、自由に設定できるようにしておきましょう。

function generateControllablePromise( 状態を完了にする関数の名前, 状態をエラーにする関数の名前 ){
    let onReady, onError;
    const promise = new Promise((res,rej)=>{ onReady = res; onError = rej; });
    promise[状態を完了にする関数の名前] = onReady;
    promise[状態をエラーにする関数の名前] = onError;
    return promise;
}

使ってみる

const 状態監視用の変数 = generateControllablePromise( '完了する', 'エラーにする' );

async function テスト(){
    console.log('開始');
    await 状態監視用の変数;
    console.log('完了!');
}

テスト();

結果:
'開始'が出力されたあと、状態監視用の変数.完了する()を実行するまで'完了!'は出力されなかった。
状態監視用の変数.完了する()を実行すると、'完了!'が出力された。)

これで、使い回しがきくようになりました。
余は満足じゃ。

このコードを使いまわしてもらえるように、
日本語部分を英語になおして、
ready/error などの単語を resolve/reject (=公式と同じ言い回し)に直したうえで
記事のトップに掲載しました。

まとめ

任意のタイミングでPromiseを完了させることができるようになったので、
「状態が完了するまで待つ」という処理を柔軟につくれるようになりました。

実はいままで、そのような処理をつくりたいときは
awaitできるsetTimeoutを1行で書く方法などを参考にして、
任意の状態になるまでwhileループでひたすら待つ、という鬼畜処理しか思いつかなかったのです。↓

const wait = async (ms)=>(new Promise(resolve => setTimeout(resolve, ms))); // awaitできるsetTimeout

async function waitSomeStatus(){
    while( !任意の状態かどうか ){
        await wait(10);
    }
}

今回Promiseの理解が深まったことで、
安心して「待つ」コードを書くことができるようになりました。

ほんとうに良かったです。
ただしちょっと疲れました。ひと眠りしようと思います…………

………

……

🪦 完

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1