LoginSignup
17
19

More than 5 years have passed since last update.

深くなるコールバックをイベントで少し解決する

Last updated at Posted at 2016-09-03

domのreadyで、ajaxでトークンが有効か確認して、有効ならajaxで一覧データが取得したい

 あると思います。
 ※長いので、本題から読んで頂いて問題ないです。

 何も考えずに書く、こんな処理になるかと。

$(document).ready(function(){
            $.ajax(
                {url:'./renew_token.php',
                 data:{
                     token:window.localStorage?localStorage.getItem('token'):"" 
                 }
                }
            ).then(function(auth_response){
                :
                $.ajax({
                    :
                }).then(function(some_response){
                    :
                })
            }).catch(function(err){
                :
            });
        });

 これ、非常に嫌いです。
 読むのもつらければ、修正するのもつらい。
 一般的には、これは関数に切り出すべきだといわれます。

    function auth(auth,callback){
        :
        callback(xxx);
        return ;
    }
    function get_list(list_conditions,callback){
        :
        callback(list_data);
    }

 一見、すっきりしたように見えて好ましいですが、果たして本当でしょうか?
 こんな感じで使う?

    auth({xxxxxx},getlist);

 まぁ今の状態ではだいぶすっきりしてますが、もし、認証に失敗したときになんらかの処理をしたい、と仕様変更があったらくそめんどくさいです。
 最初から考えとけよ、という話にもなりますが、そもそも、リストを取得するのと、認証するのは別問題であって、認証に成功しなくても特定のリストは表示したいとか、もうあとから嫌な事を言われるのが目に見えてます。

本題

 そこで、こういうのを使ってます。

emitter.js
emitter = function create_emittor(){
    var e = {};
    e.jq = $({});
    e.on = function(event,callback){
        e.jq.on(event,function(x,arg){
            callback(arg);
        });
    }
    e.trigger = function(event,args){
        e.jq.trigger(event,args);
    }

    e.proxy = function(event_success,event_error){
        if(!event_error) event_error = event_success;
        return function(err,result){
            if(!err) setTimeout(function(){e.trigger(event_success,result)});
            else setTimeout(function(){e.trigger(event_error,err)});
        }
    };
    e.proxyone = function(event){
        return function(result){
            setTimeout(function(){e.trigger(event,result)});
        }
    }
    return e;
}
var $e = emitter();

※余談にも書きましたが、これを書いたときに手元にjQueryがあったから利用しているだけで、on,triggerがあれば何でもいいです。

使い方

var $e = emitter();
$(document).ready($e.proxyone('renew_token'));

$e.on('renew_token',function(){
    $.ajax({
        url:'./renew_token.php',
        data:{
            token:window.localStorage?localStorage.getItem('token'):"" 
        },
        dataType:"json"
    })
    .then($e.proxyone('token_refreshed'))
    .catch($e.proxyone('token_rejected'));
});

//リフレッシュされたら、リストを取得
$e.on('token_refreshed',$e.proxyone('get_list'));
$e.on('token_rejected' ,$e.proxyone('redirect_to_error'));

$e.on('get_list',function(token){
    //トークンで、リストを取得
    alert(token);
    var list = {};
    //なんかリスト取得処理
    list.list = ['a','b','c'];
    $e.trigger('refresh_list',list);
});

$e.on('refresh_list',function(list){
    //もらったデータを処理
    for(var idx in list.list){
        alert(""+idx+":"+list.list[idx]);
    }
})

また、indexedDBのような非同期なDBを使用するときも、以下のような雰囲気でできます。

//サンプル関数:一度目は失敗し、二度目は成功する。
var count = 0;
function some_err_result_function(callback){
    if(count++==0) {
        callback('error');
        return;
    }
    callback(null,'ready');
}

$(document).ready($e.proxyone('init_xxxdb'));
var _db = null;
$e.on('init_xxxdb',function(){
    //DB初期化
    some_err_result_function($e.proxy('xxxdb_ready','xxxdb_error'));
});

$e.on('xxxdb_ready',function(db){
    //準備OK
    _db = db;
    alert('db_ready');
})
$e.on('xxxdb_error',function(){
    alert('db_error');
    setTimeout($e.proxyone('init_xxxdb'),500);//500ms後にもう一回チャレンジ
})

 と、割と柔軟に再試行、再描画が行えます。
 また、イベントのため、トークンがリフレッシュされたら、localStorageにも保存したい、なんて当たり前だけど忘れてた仕様の追加が起こっても、既存のロジックと完全に切り離して追加実装できます。

$e.on('token_refreshed',function(token){
    if(window.localStorage) window.localStorage.setItem('token',token);
});

 単純にリスナを追加するだけですね。

 また、サンプルにあるように、未実装のイベント(redirect_to_error)があっても特に死ぬことはないので、あとから後始末追加したりも割と簡単です。

ちょっと良いおまけ

 webworker化したり、serviceworker化するのが楽です。

余談

 on,triggerだけを行うために、jQueryを使いたくない場合は、適当に実装したバージョンもあります。

    e._handlers = {};
    e.on = function(event,eventHandler,eventHandlerName){
        if(!event)        throw 'EventName Required';
        if(!eventHandler) throw 'eventHandler Required';
        if(!e._handlers[event]) e._handlers[event] = [];
        if(eventHandlerName) eventHandler._eventHandlerName = eventHandlerName;
        e._handlers[event].push(eventHandler);
    };
    e.off = function(event,eventHandler,eventHandlerName){
        if(e._handlers[event]){
            for(var k in e._handlers[event]){
                if(!e._handlers[event].hasOwnProperty(k)) continue;
                if(eventHandler && e._handlers[event][k]==eventHandler){
                    delete e._handlers[event][k];
                }else if(eventHandlerName && e._handlers[event][k]["_eventHandlerName"]==eventHandlerName){
                    delete e._handlers[event][k];
                }
            }
        }
    };
    e.once = function(event,eventHandler){
        var handlerName = date().now + Math.random();
        var _this = this;
        e.on(event,function(){
            e.off(event,null,handlerName);
            return eventHandler.apply(t,arguments);
        },handlerName);
    };
    e.trigger = function(event,...args){
        if(!e._handlers[event]) return;
        for(var k in e._handlers[event]) if(e._handlers[event].hasOwnProperty(k)) setTimeout(function(){
            e._handlers[event][k].apply(e._handlers[event][k],args);
        },0);
    };
    //proxy以降は同一です。

 こっちのコードで動いている実績はビミョーですが、動かしてみたところなんとなくは動いているようです。
 バブリングさせたかったら、jQueryバージョンのほうがお手軽ですね。

追記

 敏感に反応してしまってすみませんが、Promise案件というTweet拝見しました。
 ちょっと言葉足らずだったかな、と言い訳補足しておきます。
 Promiseで解決した結果を、結果に意味を持たせて疎に次の処理へ続けたい、という意図があります。
(わざわざjQueryのthenとcatchを分解しているところで伝わると思っていましたが、言葉足らずでしたね。) 

17
19
1

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
17
19