Promiseでスッキリ

見出し右端の記号は?

Web Page
Booskanium's Tips.
忘れっぽい落書き人のメモ

Promiseの話は、旧式プログラマの落書き人には「なんのこっちゃ」でした。しかし仕組みがわかるとこんなスッキリしたコードで非同期処理が書けるんだという感覚にかわりました。×D 非同期処理を同期処理(逐次型)であるかの様にコーディングできるのがPromiseの利点です。
なお、このメモは旧式プログラマの落書き人のメモですので、用語や解釈のしかたがモダンではありません。:^)

Promiseを理解するのに読んだ文献:
JavaScript Promiseの本
【JavaScript】Promiseを使った非同期処理
【JavaScript】ちゃんと理解しておきたいPromiseの勘所など
Promiseを複数組み合わせる時の基本パターン(直列、並列、分岐)
Promiseとthenのメソッドチェーン(直列・並列・値の受け取り・引数)

Promiseの簡単な例

下記の2点に気づいたらスッキリと理解が進みました。
・非同期な処理をPromiseで包み(抽象化?)部品化する。
・Promiseで包んだ関数で逐次型であるかの様にthen等で組み立てる
例:非同期なXMLHttpRequest(XHR)をPromise化

//XHRをPromiseで部品化
function fetchXHR(URL) {
	return new Promise((resolve, reject) => {
		let req = new XMLHttpRequest();
		req.open('GET', URL, true);
		req.onload = function() {
			if (req.status === 200) {
				resolve(req.responseText);
			} else {
				reject(new Error(req.statusText));
			}
		};
		req.onerror = function() {
			reject(new Error(req.statusText));
		};
		req.send();
	});
}

//部品化したXHRを利用する
let URL = 'http://hogehog.xyz/webdata';
fetchXHR(URL).then((value) => {
	console.log(value);
}).catch((error) => {
	console.error(error);
});

上記を落書き人の単純な理解でメモすると、なお逐次処理っぽく説明していますが、実態はリエントラント*2です。

部品のresolveと操作のthenの関係

Promiseの中で記述してあるresolveにXHRのonload(XHRのcallback)に戻ったデータを渡すと、fetchXHR呼び出ししている操作部分のthenの所に書いた関数に結果が渡されてくる。

部品のrejectと操作のchatchの関係

Promiseの中で記述してあるrejectにXHRのonerror(XHRのcallback)に戻されたエラー理由を渡すと、fetchURL呼び出ししている操作部分のcatchの所に書いた関数にエラー内容が渡されてくる。

結局はcallbackを逐次型っぽく、

その操作の流れを解りやすく記述できるのがPromise。
上記のXHRの例のように単純なものなら、従来のcallbakでコーディングしても可読性には大差ありません。
しかし非同期処理の階層が深くなったり、複数の非同期処理の繋がり多くなると、俗に言うところのcallback地獄になりメンテナンス性が極めて悪化します。
Promiseならthenで繋げられますので見通しが良いコーディングになるのが利点です。

Promise.resolve(value)とPromise.reject(error) はPromiseの糖衣関数

例えば

Promise.resolve(42);
new Promise((resolve) => {
	resolve(42);
});
の糖衣構文です。
前者のPromise.resolveはthenableなオブジェクトをpromiseオブジェクトに変換するという機能がありますので、厳密には別物です。

例えば

Promise.reject(new Error("エラー"))
new Promise((resolve, reject) => {
    reject(new Error("エラー"));
});
の糖衣構文です。

Promisenoの直列、並列、分岐など

参考にあるウェブページ
Promiseを複数組み合わせる時の基本パターン(直列、並列、分岐)
Promiseとthenのメソッドチェーン(直列・並列・値の受け取り・引数

直列

落書き人がたどり着いた複数の非同期処理を直列につなげる書き方。

Promise.resolve()
	.then(function(){
		return new Promise(function(fulfilled, rejected){
		   asyncFunc(function(){
				~処理~
				fulfilled();
			});
		})
	})
	.then(function(){
		return new Promise(function(fulfilled, rejected){
		   asyncFunc(function(){
				~処理~
				fulfilled();
			});
		})
	})

処理が長く冗長になってしまう場合は以下の様に非同期の流れと処理部分を分ける。

Promise.resolve()
.then(asyncFunc1())			// 処理が冗長コードになる場合は外部関数にする
.then(asyncFunc2(e))		// 処理が冗長コードになる場合は外部関数にする
.then((e) => {				// 処理が短いコードの場合は直接書く
	return new Promise(function (resolve, reject) {
		~処理~
		resolve();
		~エラー時~
		reject(new DOMException('エラーメッセージ', 'OperationError'));
	});
});
.catch(err => {
	console.log(err.name + ':' +  err.message);
});
// 非同期処理1
function asyncFunc1(){
	return new Promise(function (resolve, reject) {
		~処理~
		resolve('task1 完了!');
		~エラー時~
		reject(new DOMException('エラーメッセージ', 'OperationError'));
	});
}
// 非同期処理2
function asyncFunc2(){
	return new Promise(function (resolve, reject) {
		~処理~
		resolve('task1 完了!');
		~エラー時~
		reject(new DOMException('エラーメッセージ', 'OperationError'));
	});
}

並列処理

(Promise.all)の中の処理が並列処理、それに続くthenの処理が、並列の3処理が終わったら実行される。

Promise.resolve()
    .then(function(){
        return Promise.all([
            new Promise(function(fulfilled, rejected){
                asyncFunc(function(){
                    fulfilled();
                });
            }),
            new Promise(function(fulfilled, rejected){
                asyncFunc(function(){
                    fulfilled();
                });
            })
        ])
    })
    .then(function(){
        return new Promise(function(fulfilled, rejected){
            asyncFunc(function(){
                fulfilled();
            });
        })
    });	

並列 map編

Promise.resolve()
    .then(function(){
        return Promise.all(list.map(function(item){
            return new Promise(function(fulfilled, rejected){
                asyncFunc(function(){
                    fulfilled();
                });
            }),
        }))
    })
    .then(function(){
        return new Promise(function(fulfilled, rejected){
            asyncFunc(function(){
                fulfilled();
            });
        })
    });
							

							

							

常に非同期が保証される

非同期と同期が混在すると、処理順序が入り乱れて奇っ怪な不具合を招くことがあります。
これを回避するためにsetTimeoutを差し込むような事を行う場面がありました。
そこでPromiseを使うと同期であるかの様に書けます。

処理順序が入り乱れる例:

function onReady(fn) {
    const readyState = document.readyState;
    if (readyState === "interactive" || readyState === "complete") {
        fn();		// 1
    } else {
        window.addEventListener("DOMContentLoaded", fn);	// 2
    }
}
onReady(() => {
    console.log("DOM fully loaded and parsed");
});
console.log("==Starting==");	// 3
上記の例では、すでにページがローディング済な場合に1→3という順序で動きます。
ページがローディング前の場合に3→2という順序で動きます。
これでは同じ関数を呼び出しているのに、同期処理と非同期処理が混在して、状態によって処理順序が違うという事になります。
そこで1の部分を「setTimeout(fn, 0);」という様に記述して非同期処理にしていました。しかしこの記述はJavaScript非同期の意味が解っていないと奇っ怪な記述に見えます。

Promiseで記述すると非同期処理に統一できる例:
function onReadyPromise() {
    return new Promise((resolve) => {
        const readyState = document.readyState;
        if (readyState === "interactive" || readyState === "complete") {
            resolve();		// 1
        } else {
            window.addEventListener("DOMContentLoaded", resolve);	// 2
        }
    });
}
onReadyPromise().then(() => {
    console.log("DOM fully loaded and parsed");
});
console.log("==Starting==");	// 3
Promiseを利用すると、常に3が先に実行されます。関数からの戻りは状態に関係なく、戻りはコールバックで非同期ですので、状態により処理順序が違ってしまうことはありません。

非同期処理を理解していないと

3の処理が先に動くという感覚が解らない。
何がいいたいかと云うと、白状しますがここの落書き人が初めてJavaScriptに接した時にはそうでした。callbackは知っていましたが、インラインで関数(無名関数でインライン記述)を書くというコーディング流儀には慣れ親しんでいませんでしたのでの、コーディング順に動かない挙動が解りづらくて、最初は頭がぐ~るぐる。

PromiseのCancel

PromiseをCancelしたい場合があります。例えばfetchがAbortControllerでのCancelに対応していますが、これと同じことを自前のPromise処理で行えばPromiseのキャンセルが実現できます。
具体的にはちゅんラヂで、受信準備中(Promise非同期処理)に次の選局が行われると、受信準備中処理が重複して走り、結果として選局機能が無反応になっていました。これを回避するために受信準備中のPromise処理をキャンセルさせる必要がありました。

下のボタンでPromiseのCancel動作が確認できます。
・Startで非同期処理(Promise)開始。
・Start3秒後に非同期処理を完了(resolve)させています。
・3秒経過する前にstopで非同期処理をキャンセル(reject)させています。
・Startを連打すると、稼働中のPromise処理をCancelして、新たなPromise処理を動かしています。(*1)
・Clearは動作確認表示を消去します。

  

ここに動作状況が表でされます。


多分「なんのこっちゃ」かもしれません。非同期って何?、Promiseって何? (^.^;

*1:直列動作はキューの仕掛けで

このPromiseをCancelする目的はちゅんラヂの安定化であり、1つのaudioタグを複数処理(Primise複数稼働)から叩くと動作が不定になることが解りこれを防ぐ事です。
そこでStartを連打した時に、Promise処理実行中に、Startがクリックされたら、実行中のPromise処理をCancelさせてから、次のPromise処理を実行させなければなりません。
これを実現する為に、直列実行させるキューイングの仕掛けが必要でした。

AbortControllerは1回限り

abort()メソッドでsignalのイベントが発火するのは1回限りでした。
つまりPromise非同期処理を呼び出す度に、AbortControllerをインスタンス化する必要があります。
並列処理させたいケースと考えると、リエントラントな処理(classまたはprototype)にして、AbortControllerのインスタンス化を別々にするという考慮も必要だと思われます。

ブラウザさん、誠に申し訳ございませんでした。m(_ _)m

正直に申し上げれば、ここの落書き人は、ちゅんラヂで忙しく選局したときの不安定さをブラウザが悪いということで済ませていました。なんでって、ブラウザによって不安定さの症状が異なったからです。

リバースエンジニアリングは

詳しくはこの上述で確認ボタンの処理を記述したJavaScript「js/promise_cancel.js」をリバースエンジニアリングしてください。UTF-8(UNICODE)でデコードしないと文字化け化けです。
このコードで利用しているPromiseは非同期処理におけるCallback地獄から開放してくれる救世主です。

AbortControllerはDOMです

PromiseのCancelにDOMのイベント処理を利用するのは、ちょっと強引な気もします。
しかしnode.jsにもAbortControllerが実装されましたので、PromiseのCalcelにAbortControllerを使うのは実質的にスタンダードな方法です。
※純粋なJavaScriptだけで「cancelable promises」が議論されたこともあったようですが頓挫した様です。