JavaScript / Basic / Promise

JavaScript / Basic / Promise

界隈ではコールバック地獄を回避する記述方法と言われているが実際のところなんなのか

同期非同期とか関係ない部分から入っていかないとだめだと思う

しかしながら、JavaScript / Basic / Promise / async と await を覚えれば処理を設計するわけでなく単に使うだけの我々凡人は十分という感じもする。

ザックリ理解

Promise というのは特殊関数宣言の一種だと思えばヨイ。 その特殊関数は実行される場所が決まっていてそれは、thenメソッドの内部だけである。

then メソッドで実行されたこの特殊関数は resolve か reject 関数を内部で実行することになる。 この関数が実行されるまでこの特殊関数は待ち状態になり実行が先に進まなくなる。 つまり、ここで記述が同期風になる。

resolve が実行された場合その処理は次の then に進む。 reject が実行された場合その処理は次の catch へ進む。

そして then に渡す関数は基本的に二重構造になっていて、resolve 実行時にパラメータを受け取る外側と、次の実行へ繋ぐ promise 生成の内側になっている。

そこでは、内部がどんな入れ子構造になっていようがなんだろうが、resolve するまでホールド。

基本スタイル

ほぼこれである。これが Promise を使うための基礎である。promise を単独で突然 new したりすることはほぼ無い。 必ず起点となる関数の return として new される。必ず return とセット。 promise は記述が煩雑なのでそのためのショートカット記法があるが、そのショートカットも内部はコレである。

理解としては、「コールバックとかなんとか知らんよ、俺は非同期処理なんかしたくないし考えたくもない普通に処理をズラズラ並べて書きたいだけ」の仕組みが Promise であると。

これを理解すると Promise の使い方は基本的に下記のようになる。Promise を返す関数を作ってその Promise に非同期処理を含む関数を渡す。

function hoge(){
    return new Promise(function(resolve, reject){
        // piyo 何か非同期っぽい処理 でコールバック関数を取る
        piyo(function(){
            // そのコールバックで resolve 実行
            resolve();   // resolve の実行
        }); 
    });
}

これをこのように使う

hoge().then(hoge);

Promise が作成される時点で、それに渡す関数は実行される。つまり現在 Promise でラップしている非同期処理が実行されることになる。 ということはつまり一番外側の hoge を実行すると非同期処理が実行されることと同じである。

非同期処理(piyo関数)が終わるとそのコールバックがキックされてその結果 resolve が呼び出される。 resolve が呼び出されるとそれは then 関数の実行されていて、それに渡した関数が実行可能状態になるというイベントに対してこいつがフックされる。 フックされるだけなので、resolve 以降も処理は先に進んでいく。

JS の処理の流れ的には、非同期関数(piyo関数)が実行されるより先に then 関数が実行されていて、渡した関数がもうすでに実行可能状態になっているので、 非同期処理が終わったら即 then に渡した関数が動くように振る舞う

この関数は then に渡している。つまりこの関数自体が resolve にあたるので、この関数のシグネチャが resolve のシグネチャと一致することになる。 つまりこの関数がパラメータを受け取るように設計し、resolve に非同期処理の結果を渡せば処理を同期的に数珠つなぎにできるとなる。

しかし、世間でよく見る Promise を使われていると言われているコードには new Promise など1行も出てこない。 なにこれと思ったら↓の「then に渡す関数の仕様」を読むとその仕組がわかる。

キーワード

Promise, reject, resolve, Promise を返す

Promiseとは

そういう特殊なオブジェクトで作成時に、成功時の処理と失敗時の処理の2つを引数に取る関数を引数にとる。 何に使うかは別としてそういうもんだと覚える。

なのでこうなる

var a = "hello";
var b = "world";
var hoge = new Promise(function(resolve, reject){
    if(a == b){
        resolve();
    }else{
        reject();
    }
});

Promise に渡す関数とは

new Promise 時に渡す関数は自動的に非同期処理の関数となる。 つまり Promise が作られた時点でそのコールバックとして待つ存在となる。

このようなサンプルを書くとわかるが

var p = new Promise(function(a, b){
    console.log(a());
});
p.then(function(){
    return "hello";
});

この Promise p を実行すると答えは undefined になる。

渡した関数がそのまま非同期で実行されるのではなく、なんらかのラップがかかった後、実行ということになるようだ。

設計としては、実行の主体は then に渡す関数にあるべきで、 Promise 側は判断を書けという話だ。

Promise を実行する

手っ取り早く実行するにはpromise の then メソッドを呼んでやればいいだろう。

このようなものを書いてみた。

var p = new Promise(function(a, b){
    console.log("do promise");
    a();
});
console.log("start");
p.then(function(){
    console.log("in then");
});
console.log("end");

これを動作させると。このような実行順番になる。

do promise
start
end
in then

つまり、Promise に渡す関数はその作成時に実行されるが、then に渡した関数は実行はされるが待ち状態に入る。 無期限の setTimeout のように振る舞うようだ。

次の then の実行はその無期限の setTimeout の文脈で動くようで、ここも非同期に進んでいく

パターンとして Promise に渡す処理が遅くて、then の実行よりもかかる場合、よくあるコールバックのパターンがこれ。 これが機構としてうまく動きそうなのはわかる。

スゴイのが、Promise に渡す処理が十分に早くて then の実行よりも先に動作してもちゃんと動くことである。 存在しない関数を、あたかももうあるが如くに非同期に実行してしまう(無期限 setTimeout)

Promise とはこういう動作をするようだ。

このことからも、よく Promise の処理では Promise そのモノではなく Promise を返す関数が用いられることがわかるだろう。 別々で書くと、実際の実行とその反応を書く部分が離れてしまうからだ。 それにオブジェクトの生成と処理の実行は感覚的には別物であくまで非同期の「処理」をしているんだという使用感のズレが無いようにしているのだ。

then に渡す関数の仕様

基本的には Promise 基本形の Promise を返す関数である。

しかし then に渡す関数は乱暴に言うと resolve なので、関数ならなんでもよいということになっている。 実際に事例を見ても、適当に関数を渡しているだけなのになぜか Promise の機構の中でちゃんと動いている。

では、なぜこのような「Promise を返す関数でなく関数ならばなんでもよい」記述ができるかというと、then に渡す関数の戻り値の型によって挙動が変わるからである。 then に渡す関数が単に何かの処理だけをするならよいが、return を返して、しかもそれが Promise で無い場合、特殊な動作となる。

この場合、return した値で resolve する 関数を受け取る Promise が新規作成されて返す関数になる。 ややこしいが、つまりコードで表すと、

このように書くと・・・

hoge().then(function(){
    return "ほげほげ";
});

・・・内部としては、下記のような振る舞いの関数として解釈される。

hoge().then(function(){
    var v = "ほげほげ";
    return new Promise(function(resolve, reject){
        resolve(v);
    });
});

これが何が便利かというと、まず関数を書くだけなのに then のチェーンで処理を継続できる。 そして、return の記述があたかも次の then へのトリガーのように扱えるということにある。

規定通り promise を返す関数であるなら、このような変換はおこらない。

Promise.resolve

Promise には静的メソッドが実装されていていつくか機能がある。

この Promise.resolve は new Promise のショートカットになっていてよく使われる。

つまり、このようになる。

function hoge(){
    var v = "hogehoge"
    return new Promise(function(resolve){
        resolve(v);
    });
}
 
function hoge(){
    return Promise.resolve(function(){
        return "hogehoge";
    })
}

つまり、then 関数に渡す関数のような記述がいきなり書けるというパターンである。

Promise.all

通常 then は 1つのpromise の実行完了まで then が待つという状態を作るが、これを使うと、複数の promise の全完了を待ってから次の then に進めるという動作となる。

動作としては Promise.resolve と同じで、promise を返すのである。

つまり待ちたい処理を作る立場とすれば、複数の promise をこいつに渡すとそれが全部完了するまでの promise を新しく生成してくれると思えばよい

あとの考え方は、今までと同じである。となると簡単な例はこうなる

function hoge(){
    return new Promise(function(resolve, reject){
        resolve();
    });
}
function hoge1(){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log("hoge1");
            resolve("from hoge1");
        }, 2000);
    });
}
function hoge2(_m){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log(_m + " hoge2");
            resolve(_m + " to hoge2");
        }, 1000);
    });
}
function hoge3(_m){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log(_m + " to hoge3");
            resolve();
        }, 3000);
    });
}
 
function hoge4(){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log("hoge4");
            resolve();
        }, 1000);
    });
}
 
function hogeAll(){
    var pList = [
        hoge1(),
        hoge2("from hogeAll"),
        hoge3("from hogeAll")
    ];
    return Promise.all(pList);
}
 
// この時点では hogeAll は実行されてないよ。
hoge()
    .then(hogeAll)
    .then(hoge4);

こいつを実行すると。まず hoge の空の Promise が実行され、これは内部に処理が無いので次に hogeAll が実行される。 ここで3連続で promise 生成のメソッドをキックして配列に詰める。それを Promise.all を用いて全完了待ちの promise を生成する。 当然この生成した Promise を return する。

promise は生成した時点で実行状態に入るので hoge1, hoge2, hoge3 が同時並行に動き始める。 一番最初に完了するのは hoge2, 次に hoge1 次に hoge3 になる。

当然 console.log はその順番で出る。ここでは hoge3 が処理時間としては最長なので、hoge3 が終わってから次の then の処理である hoge4 が処理される。

統一した Promise を新規に作って返すというところを意識するところがポイントである。

Promise.race

all が全員待ちましょうだったのに対し、race はイチ抜けという処理である。つまり全部実行するけど、最初のやつが終了したら次進むというやつである。

all のサンプルを race に変えると動作するので試してみる。

この処理では hoge2 が一番処理時間が短いのでこれが終わると hoge4 に進んでしまう hoge4 は hoge2 の1秒後に動作するということになる。 つまり、処理の開始から2秒後である。これは hoge1 の処理時間と同じなので hoge1 と hoge4 が同時に完了する。 そして最後に hoge3 の結果が出る。

ポイントはイチ抜けで次に進む、しかし他のやつも実行は続けるというやつである。

Promise により非同期実行される予定の処理の設計

現状、よくわからん。

javascript/basic/promise/start.txt · 最終更新: 2020-08-04 12:45 by ore