Pull to refresh

Удобный callback

Reading time 8 min
Views 3K
Надоело каждый раз писать колбеки руками. Написал простенькую скриптину, которая запонимает функцию (функции) с массивом аргументов и контекстом в объекте с методом fire, который не зависит от this, чтобы можно было цеплять колбек не только в «чистом» коде, но и к онклику или таймеру. Набор исполняемых функций и аргументов/контекста к каждой из них произвольный.

Из пряников только возможность давить всплытие ошибок, ограничивать кол-во запусков колбека и удобство выпечки других пряников на свой вкус.

Сама скриптина

function _callback(fn, args, ctx, decorators) {
    // создание колбек-объекта
    // fn - вызываемая функция (обязательна):
    //  может быть функцией, объектом {fn: function, args: [], ctx: {}}, или массивом
    //  или смесью массивов, объектов и функций
    // args - массив аргментов
    // ctx - контекст (this) для вызываемой функции
    // decorators - строчка или массив строчек с назаванием дектораторов,
    //  дополняющих основную функциональность
    var ret = new _callback.Def(fn, args, ctx);
    if (decorators) {
        var d = _callback.decorators;
        for (var key in decorators) {
            if (key in d) ret = new d[key](ret, fn, args, ctx, decorators[key]);
        }
    }
    return ret;
}
_callback.Def = function(fn, args, ctx) {
    var dublicate = this; // myClb.fire можно просто передать в таймер или dom-событие,
                         // и ничего от этого не сломается
    var arrayOrNull = function(ar) { return ar && ar.constructor == Array ? ar : null; };
    
    setFn(fn);
    
    this.updateFn = updateFn;
    //this.__defineSetter__("fn", setFn); // ie
    //this.__defineGetter__("fn", function() { return fn; });
    
    function fireByFn(myArgs, myFn, myCtx) {
        // вызываем колбек-функцию только здесь
        (myFn || fn).apply(myCtx || ctx || null, arrayOrNull(myArgs) || arrayOrNull(args) || []);
        return dublicate;
    }
    function fireByArray(myArgs, myFn, myCtx) {
        // если вместо фукнции прилетел массив, выполним вызов для каждого элемента
        for (var i=0, ar=myFn||fn, c=myCtx||ctx, a=arrayOrNull(myArgs) || arrayOrNull(args), l=ar.length; i<l; i++) getFireFn(ar[i])(a, ar[i], c);
        return dublicate;
    }
    function fireByJSON(myArgs, myJson, myCtx) {
        // если вместо фукнции прилетел JSON
        var json = myJson || fn;
        getFireFn(json.fn)(arrayOrNull(json.args) || arrayOrNull(myArgs), json.fn, json.ctx || myCtx);
        return dublicate;
    }
    
    function getFireFn(fn) {
        // определяем тип обратного вызова и возвращаем его
        var type = dublicate._getTypeofFireFn(fn), fireFn;
        switch(type) {
            case "json": return fireByJSON; break;
            case "array": return fireByArray; break;
            case "function": return fireByFn; break;
        }
        return fireFn;
    }
    
    function setFn(val) {
        // отдаем наружу правильный метод fire
        dublicate.fire = getFireFn(fn = val);
    }
    function updateFn(transformFn) {
        setFn(transformFn(fn));
        return dublicate;
    }
};
_callback.Def.prototype._err = function() { this._err.withoutFn(); };
_callback.Def.prototype._err.withoutFn = function() {
    throw "_callback: wrong fn argument";
};
_callback.Def.prototype._getTypeofFireFn = function(fn) {
    // чем является переданный объект: функцией, массивом, объектом
    if (!fn) this._err.withoutFn();
    if (typeof fn == "function") return "function";
    if (fn.constructor == Array) {
        for (var i=fn.length; i--;) this._getTypeofFireFn(fn[i]);
        return "array";
    } else {
        // {}
        this._getTypeofFireFn(fn.fn);
        return "json";
    }
    this._err.withoutFn();
};
_callback.decorators = {};
_callback.decorators._copy = function(component, orig, instance) {
    for (var key in component) orig[key] = instance[key] = component[key];
    //instance.__defineSetter__("fn", function(val) { component.fn = val; }); // ie
    //instance.__defineGetter__("fn", function() { return component.fn; });
};
_callback.decorators.count = function(component, fn, args, ctx, misc) {
    var orig = {};
    _callback.decorators._copy(component, orig, this);
    var fired = misc;
    
    this.fire = fire;
    
    function fire() {
        if (!fired) return;
        fired--;
        component.fire();
    }
};
_callback.decorators.stopThrow = function(component, fn, args, ctx, misc) {
    var orig = {};
    _callback.decorators._copy(component, orig, this);
    this.fire = fire;
    
    function fire() {
        try {
            component.fire();
        } catch(er) {}
    }
};
_clb = _callback;


* This source code was highlighted with Source Code Highlighter.


Пример использования

Подготовим фукнции, которые будут откликаться при .fire
var res = "";
addLine = function(str) {
    res += str +"\n";
};

function f1(q, w, e) {
    addLine("f1: "+ q +", "+ w +", "+ e +"; this.q = "+ this.q);
}
function f2(q, w, e) {
    addLine("f2: "+ q +", "+ w +", "+ e +"; this.q = "+ this.q);
}
function f3(q, w, e) {
    addLine("f3: "+ q +", "+ w +", "+ e +"; this.q = "+ this.q);
}
function f4(q, w, e) {
    addLine("f4: "+ q +", "+ w +", "+ e +"; this.q = "+ this.q);
}
function f5() {
    uNdEfInEd++;
    addLine("f5");
}


* This source code was highlighted with Source Code Highlighter.


И, собственно пример использования
// создадим пройстой объект колбека:
q = _clb(
    f1,     // функция, которую следует вызывать...
    [1,2,3], // ...с тремя аргументами и...
    {q:5}    // ...в заданном контексте (this.q==5)
);
q.fire(); // просто выполним колбек
 // f1: 1, 2, 3; this.q = 5
q.fire([8]); // выполним колбек с другими аргументами
 // f1: 8, undefined, undefined; this.q = 5
q.fire(false, false, {q:11}); // функция и аргументы теже, но другой this
 // f1: 1, 2, 3; this.q = 11
q.fire([]); // и без аргументов
 // f1: undefined, undefined, undefined; this.q = 5
addLine("----"); // для удобочитаемости
// переобозначим колбэк
q = _clb([f1, f2, f3], [1,2,3], {q:5}); // аргументы и контекст теже, но вызываться будут три фукнции
q.fire();
// f1: 1, 2, 3; this.q = 5
// f2: 1, 2, 3; this.q = 5
// f3: 1, 2, 3; this.q = 5
q.fire([8]); // аргументы меняются вне зависимости от "функции"
// f1: 8, undefined, undefined; this.q = 5
// f2: 8, undefined, undefined; this.q = 5
// f3: 8, undefined, undefined; this.q = 5
addLine("----");

// можно использовать и JSON вперемешку с массивами и переопределить аргументы для отдельных вызовов:
q = _clb(
    [
        f1, // f1: 1, 2, 3; this.q = 5
        {fn: f2, args: [4, 5, 6]}, // f2: 4, 5, 6; this.q = 5
        {
            fn: [
                f2, // f2: 7, 8, 9; this.q = 0
                {fn: f2, args: [4, 5, 6]}, // f2: 4, 5, 6; this.q = 0
                f3 // f3: 7, 8, 9; this.q = 0
            ],
            args: [7, 8, 9], // аргументы по умолчанию для этого блока
            ctx: {q: 0} // контекст по умолчанию для этого блока
        }
    ],
    [1,2,3], // аргументы по умолчанию
    {q:5}    // контекст по умолчанию
).fire();
addLine("----");

// заменять список функий можно так:
q.updateFn(function(fn) {
    // мы знаем, что у нас массив
    fn.splice(1, 5);
    fn.push(f1, f1);
    return fn;
}).fire(); // трижды: f1: 1, 2, 3; this.q = 5
addLine("----");

// если не нужен ie, можно раскомментировать __defineSetter__ и __defineGetter__ в скрипте и делать так:
//q.fn = [f2, {fn: f2, args: [], ctx: {}}, {fn: f3, args: [7, 8, 9], ctx: {q: 0}}];
//q.fire();
//addLine("----");

// запрещаем всплытие ошибок:
q = _clb(f5, false, false, {stopThrow: true}).fire(); // ничего не выведется

// ограничиваем кол-во исполнений колбэка:
q = _clb(f1, [1,2,3], {q:5}, {count: 1});
q.fire(); // f1: 1, 2, 3; this.q = 5
         // счетчик переключается с 1 на 0
q.fire(); // второй раз колбек не выстрелит

alert(res);


* This source code was highlighted with Source Code Highlighter.
Думаю, я не один такой умный и хотелось бы узнать другие варианты.
Tags:
Hubs:
-1
Comments 20
Comments Comments 20

Articles