LoginSignup
9
8

More than 3 years have passed since last update.

async/await は Promise を置き換えることは出来ると思う その 2

Last updated at Posted at 2020-12-14

前の記事:async/await は Promise を置き換えることは出来ると思う

前提

「Promise を使ったコード」の定義は hoge().then(x => { ... }).catch(x => { ... }).finally(...); のようにメソッドチェーンを使って書かれた非同期処理であって、Promise という文字列がコード内に登場したらダメという意味ではないです。おそらく、この記事を書く発端となった記事は Promise クラスが登場するコードになると思うので、この先に書いてあるコードのほとんどは Promise を使ったコードになると思います。言葉って難しい…。

というわけで頭の体操がてら async/await で書けないと言われた処理を async/await 使って書いてみようと思います。中には無理やり感あるのも出てくる可能性もありますが、頭の体操ということで。

複数 API に並列にリクエストを投げて一つ以上成功した時だけ先に進むみたいな問題

Promise.any を await します。(Promise.any 正式に来ないかな…)
ここでは bluebird を使いました。

// 先頭にこれがある import Promise from 'bluebird';

const errorFunction = () => {
    return new Promise<string>((resolve, reject) => {
        setTimeout(() => reject(new Error('error')), 1000);
    });
};
const successFunction = () => {
    return new Promise<string>((resolve, reject) => {
        setTimeout(() => resolve('done'), 2000);
    });
};

const result = await Promise.any([errorFunction(), successFunction()]);
console.log(result); // 結果 done

複数の API に問い合わせて、全ての結果が返ってきたら結果を集約して表示する。どれかが失敗した場合はエラー処理をする

Promise.all を await します。

const api1 = () => {
    return new Promise<string>((resolve, reject) => {
        setTimeout(() => resolve('result1'), 1000);
    });
};
const api2 = () => {
    return new Promise<string>((resolve, reject) => {
        setTimeout(() => resolve('result2'), 2000);
    });
};

try {
    const [api1Result, api2Result] = await Promise.all([api1(), api2()]);
    console.log(`${api1Result}, ${api2Result}`); // result1, result2    
} catch (error) {
    // エラー処理
}

ある非同期処理について、そこに繋がる前処理や後処理が2つ以上ある場合 (N > 2) は async/await は適用できない

どっちも Promise.allPromise.allSettled の適切なものを使えば良さそう。

const preProcess = async (value: string): Promise<string> => {
    return new Promise<string>((resolve) => setTimeout(() => { resolve(value); }, 1000));
};
const postProcess = async(inputForPostProcess: string, outputOfPreProcess: string): Promise<string> => {
    return new Promise<string>((resolve) => setTimeout(() => { 
        resolve(`${inputForPostProcess} ${outputOfPreProcess}`); 
    }, 1000));
};

// 前処理が複数は Promise.all を await
const [out1, out2, out3] = await Promise.all([preProcess('in1'), preProcess('in2'), preProcess('in3')]);
const outputs = await Promise.allSettled([
    postProcess('post1', out1),
    postProcess('post2', out2),
    postProcess('post3', out3),
]);

if (outputs.every((x) => x.status == 'fulfilled')) {
    console.log(`${outputs[0].value}, ${outputs[1].value}, ${outputs[2].value}`);
}

ある非同期処理について、そこに繋がる前処理や後処理が2つ以上ある場合 (N > 2) は async/await は適用できない + 後処理は個別に完了次第何か処理をしたい

const preProcess = async (value: string): Promise<string> => {
    return new Promise<string>((resolve) => setTimeout(() => { resolve(value); }, 1000));
};
const postProcess = async(inputForPostProcess: string, outputOfPreProcess: string): Promise<string> => {
    return new Promise<string>((resolve) => setTimeout(() => { 
        resolve(`${inputForPostProcess} ${outputOfPreProcess}`); 
    }, 1000));
};

const postProcessHandler = async(postProcess: Promise<string>) => {
    try {
        const r = await postProcess;
        // 何か正常処理  
    } catch (error) {
        // 何かエラー処理
    }
}

// 前処理が複数は Promise.all を await
const [out1, out2, out3] = await Promise.all([preProcess('in1'), preProcess('in2'), preProcess('in3')]);
// postProcess の結果から何か別のことをする関数を Promise.all などでまとめて await
// そもそも待つ必要がないなら fire and forget でもいい
await Promise.all([
    postProcessHandler(postProcess('post1', out1)),
    postProcessHandler(postProcess('post2', out1)),
    postProcessHandler(postProcess('post3', out1)),
]);

上の例で、後処理がN個あって、さらに後処理の結果を処理するのは別の関数にやらせたい

const innerFunction = async(value: string): Promise<[Promise<string>, Promise<string>, Promise<string>]> => {
    const preProcess = async (value: string): Promise<string> => {
        return new Promise<string>((resolve) => setTimeout(() => { resolve(value); }, 1000));
    };

    const postProcess = async(inputForPostProcess: string, outputOfPreProcess: string): Promise<string> => {
        return new Promise<string>((resolve) => setTimeout(() => { 
            resolve(`${inputForPostProcess} ${outputOfPreProcess}`); 
        }, 1000));
    };

    // 前処理が複数は Promise.all を await
    const [out1, out2, out3] = await Promise.all([preProcess('in1'), preProcess('in2'), preProcess('in3')]);
    // postProcess の結果から何か別のことをする関数を Promise.all などでまとめて await
    // そもそも待つ必要がないなら fire and forget でもいい
    return [
        postProcess('post1', out1),
        postProcess('post2', out1),
        postProcess('post3', out1),
    ];
};

// 各々やりたい処理
const fn1 = async(p: Promise<string>) => {
    try {
        const output = await p;
        console.log(`fn1: ${output}`); // 正常処理
    } catch (error) {
        // エラー処理
    }
};
const fn2 = async(p: Promise<string>) => {
    try {
        const output = await p;
        console.log(`fn2: ${output}`); // 正常処理
    } catch (error) {
        // エラー処理
    }
};
const fn3 = async(p: Promise<string>) => {
    try {
        const output = await p;
        console.log(`fn3: ${output}`); // 正常処理
    } catch (error) {
        // エラー処理
    }
};

// 非同期処理を3つ開始してもらって
const [p1, p2, p3] = await innerFunction('okazuki');
// 後はお好きなように(今回は fire and forget)
// でも、個人的には多くの場合 await することになると思う
fn1(p1);
fn2(p2);
fn3(p3);

fn1, fn2, fn3 を呼び出してる場所で関数を別途定義せずにラムダ式でさくっと書いてしまいたい場合は…こうとか??

const innerFunction = async(value: string): Promise<[Promise<string>, Promise<string>, Promise<string>]> => {
    const preProcess = async (value: string): Promise<string> => {
        return new Promise<string>((resolve) => setTimeout(() => { resolve(value); }, 1000));
    };

    const postProcess = async(inputForPostProcess: string, outputOfPreProcess: string): Promise<string> => {
        return new Promise<string>((resolve) => setTimeout(() => { 
            resolve(`${inputForPostProcess} ${outputOfPreProcess}`); 
        }, 1000));
    };

    // 前処理が複数は Promise.all を await
    const [out1, out2, out3] = await Promise.all([preProcess('in1'), preProcess('in2'), preProcess('in3')]);
    // postProcess の結果から何か別のことをする関数を Promise.all などでまとめて await
    // そもそも待つ必要がないなら fire and forget でもいい
    return [
        postProcess('post1', out1),
        postProcess('post2', out1),
        postProcess('post3', out1),
    ];
};

// 突き放しで呼ぶだけ
const fireAndForget = <T>(f: () => Promise<T>) => f();

// 非同期処理を3つ開始してもらって
const [p1, p2, p3] = await innerFunction('okazuki');
// 後はお好きなように(fire and forget)
fireAndForget(async() => {
    try {
        const output = await p1;
        console.log(output);
    } catch (error) {
        // error
    }
});
fireAndForget(async() => {
    try {
        const output = await p2;
        console.log(output);
    } catch (error) {
        // error
    }
});
fireAndForget(async() => {
    try {
        const output = await p3;
        console.log(output);
    } catch (error) {
        // error
    }
});

fire and forget で突き放しで呼んでしまうと例外処理きちんとしないと不幸が起きるので注意

1つの画像に対して複数のフィルター(非同期処理)を適用した結果を表示する

/* 出力
now loading
now loading
now loading
done { index: 1, result: filter2: src image }
done { index: 0, result: filter1: src image }
done { index: 2, result: filter3: src image }
filter1: src image
filter2: src image
filter3: src image
*/

async function main() {
    // filter の元
    const filter = async(image: string, filterName: string, interval: number) => {
        return new Promise<string>((resolve) => setTimeout(() => resolve(`${filterName}: ${image}`), interval));
    }
    // 画像処理はめんどくさいので文字列加工をフィルターに見立てて3つ作る
    const filter1 = (image: string) => filter(image, 'filter1', 3000);
    const filter2 = (image: string) => filter(image, 'filter2', 2000);
    const filter3 = (image: string) => filter(image, 'filter3', 5000);

    // ページに見立てたクラス
    class Page {
        private images: string[]
        constructor(
            // 元データ
            public sourceImage: string, 
            // このページで扱うフィルター
            public filters: ((image: string) => Promise<string>)[]) {
            // フィルターと同じ要素数だけ結果格納用配列を用意しておく
            this.images = [...Array(filters.length)].map(_ => 'now loading');
        }

        async applyFilters() {
            // とりあえず手抜き実装でエラーの有無に関係なく全フィルターを適用して、終わったものから終わった時の処理をする
            await Promise.allSettled(
                this.filters.map(async (f, i) => {
                    const result = await f(this.sourceImage);
                    this.images[i] = result;
                    this.notifyUpdate(i, result);
                }));
        }

        // 指定要素のインデックスの処理結果を表示
        notifyUpdate(index: number, result: string) {
            console.log(`done { index: ${index}, result: ${result} }`);
        }

        // 全データ表示
        printAll() {
            for (const image of this.images) {
                console.log(image);
            }
        }
    };

    // ページ作って、処理前の状態を印字して、フィルター適用(途中経過表示)して
    // フィルター適用が全部終わったら再度全部表示してみる
    const page = new Page('src image', [filter1, filter2, filter3]);
    page.printAll();
    await page.applyFilters();
    page.printAll();
}

main();

まとめ

思いつき次第足していくかも。

9
8
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
9
8