2019-06-07に更新

PromiseがよくわからなかったのでオレオレPromiseを作って雰囲気を掴む

非同期がわかりません。
「この関数はPromiseを返します」
Promiseって何だよ。
非同期やってもコールバック地獄にならないらしいけど、
結局引数に渡すのはコールバックだし、
僕からするとまーまーわからんのです。

というわけでPromiseを自作して雰囲気を掴むことにしました。

注意

あくまで雰囲気を掴むだけです。実際の実装は微塵も知りません。
いろんなドキュメントを読んだ上での筆者の解釈(を筆者の実装レベルでできるとこだけ再現したもの)です。とんちんかんかもしれません。
妥協しまくりです。
Promise.allとかやりません。それどころかPromiseチェーンすらできません。
catchは実装したけど試してません。
(要はほとんどできてないじゃないか)

あまりにもとんちんかんなこと言ってたら教えて下さい。
信用しないでください。
レベル不足でクソコードです。

とりあえずコード

とりあえずよく呼び出す側のコードです。

<html lang="ja">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>オレオレPromise</title>
        <script type="text/javascript" src="promise.js"></script>
        <script type="text/javascript" src="index.js"></script>
    </head>
    <body>
        hello async
        <button onclick="wait()">
            async start
        </button>
        <p id="start" style="display:none"></p>
        <p id="async" style="display:none"></p>
        <p id="finish" style="display:none"></p>
    </body>
</html>
const wait = ()=>{
    const start = document.getElementById('start');
    start.style="";
    start.innerHTML += "start:" + new Date();
    hidouki().then(finish).catch(dispError);

}

const hidouki = ()=>{

    return new Promise(wait1);
    //return new oreorePromise(wait1); //こっちに書き換えても動くようになるのが目標

}

const wait1 = (resolve,reject) => {
    setTimeout(() => {
        const async1 = document.getElementById('async');
        async1.style="";
        async1.innerHTML += "wait3 sec :" + new Date();
        resolve('succeeded async');
    },3000);
}

const finish = (value) => {
    setTimeout(()=>{
        const finish = document.getElementById('finish');
        finish.style = "";
        finish.innerHTML += value + " :" + new Date();
    },5000);
}

const dispError = (error) => {
    alert(error);
}

WS000004.JPG
ちゃんと待ってる。

さて、new Promiseしているところを自作Promiseに書き換えても動くようにするのが目標です。

わからんなりの実装

正直、Promiseのコンストラクタの引数はコールバック関数で、そのコールバック関数の引数はresolveとrejectで……、
って言われてわかる? みんなすごいね! 僕はわかんないです。そのresolveとrejectはどこから来たんだよ。
ってのを試行錯誤しながら理解しようとした結果、以下のようになりました。

class oreorePromise{
    constructor(callback){
        this.value = null;
        this.state = 'pending';
        this.thenTimer = [];
        this.catchTimer = [];

        callback(this.resolve.bind(this),this.reject.bind(this));

    }

    resolve(value){
        this.value = value;
        this.state ='fulfilled';
    }

    reject(value){
        this.value = value;
        this.state='rejected';
    }

    then(nextCallback){
        this.thenTimer.push(setInterval(function(){
            if(this.state === 'fulfilled'){
                this.value = nextCallback(this.value);
                clearInterval(this.thenTimer.shift());
            }
            else if(this.state === 'rejected'){
                //待機やめる
                clearInterval(this.thenTimer.shift());
            }
        }.bind(this),100)); //テストだし100ミリ秒くらい待とう

        // https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Using_promises#Chaining
        // then 関数は元の Promise とは別の新しい Promise を返します。
        // らしいけど、もう力尽きた…。
        return this;
    }

    catch(errorCallback){
        this.catchTimer.push(setInterval(function(){
            if(this.state === 'fulfilled'){
                //待機やめる
                clearInterval(this.catchTimer.shift())
            }
            else if(this.state === 'rejected'){
                errorCallback(this.value);
                clearInterval(this.catchTimer.shift())
            }
        }.bind(this),100));
    }
}

JavaScriptのthisがわかんなくて、ただでさえわからないのにコールバックしまくってthisが迷子になって結構しんどかった。一応動いたけど。
Promiseよりthisの方が難しかった。
だれかthis教えて。

resolveとrejectはPromiseオブジェクトが持つメソッドで、
「コンストラクタに引数として渡したコールバック関数を実行するときに、resolveとrejectを引数として実行しますよ。
なので、実行したい関数を定義するときにresolveとrejectが引数になるように定義してくださいね」
というお約束だということが自分で書いてようやくわかった。

わかってからだと、ちゃんと書いてあることがわかる。なぜ最初から見つけられないのだ。

どこかの解説記事で、
「resolve関数を実行するとthenメソッドが呼び出される」
とか書いてあって腑に落ちなかったんだけど、たぶん、正しいのは、
「thenメソッドは即座に呼ばれていて、オブジェクトの持つ状態(oreorePromiseではプロパティstate)がfulfilledになるまで、引数として渡されたコールバック関数の実行を待つ」
なんだと思う。
(自信はない)
resolveは状態を変更しているだけで、別にthenに渡された関数を実行しているとかはない、と思う。(ここは特に自信ない)

最初にそう言う説明を見てしまったのでthenメソッドが魔法のように見えてしまっていたのだけれど、
(MDNで「さあ魔法の時間です。」とか言ってるしね!)
よく見ればobject.method()という至極基本的な文法だし、即座に呼ばれて当然だよなと。

このコードでは、状態が変わるまでsetIntervalでループしてるのは最高にダサいと思うけど、本質とずれるのであまり凝ってない。

できたので書き換えてみる

const hidouki = ()=>{

    //return new Promise(wait1);
    return new oreorePromise(wait1); //こっちに書き換えても動くようになるのが目標

}

WS000005.JPG

動いた。

まとめ

ドキュメントを見返して、ソースを見返す度に「あ、ここ違うな」ってなる。
ただ、重ね重ねだけど、雰囲気を掴んでPromiseを使えるようになるのが目的で、Promiseのコピーを作りたいとかではない。
せめてPromiseチェーンは実装したかったけど、費用対効果が薄すぎてやめた。
個人的には

resolveとrejectはPromiseオブジェクトが持つメソッドで、
「コンストラクタに引数として渡したコールバック関数を実行するときに、resolveとrejectを引数として実行しますよ。
なので、実行したい関数を定義するときにresolveとrejectが引数になるように定義してくださいね」
というお約束だということが自分で書いてようやくわかった。

どこかの解説記事で、
「resolve関数を実行するとthenメソッドが呼び出される」
とか書いてあって腑に落ちなかったんだけど、たぶん、正しいのは、
「thenメソッドは即座に呼ばれていて、オブジェクトの持つ状態(oreorePromiseでは変数名state)がfulfilledになるまで、引数として渡されたコールバック関数の実行を待つ」
なんだと思う。
(自信はない)

ここが理解できた時点でもういいかって感じ。おまけで完成させた。

まとめ2

みんな、async/await使おうぜ!

まとめ3

だれかthis教えて


hammhiko

恥を晒して生きていきます。

Crieitは個人で開発中です。 興味がある方は是非記事の投稿をお願いします! どんな軽い内容でも嬉しいです。
なぜCrieitを作ろうと思ったか

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください!

ボードとは?

関連記事

コメント