JavaScript

индекс
246,38

Удобный callback

Надоело каждый раз писать колбеки руками. Написал простенькую скриптину, которая запонимает функцию (функции) с массивом аргументов и контекстом в объекте с методом 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.
Думаю, я не один такой умный и хотелось бы узнать другие варианты.
–1
21 июля 2010, 15:54
12

комментарии (20)

+11
Terion #
Извините, но я ничерта не понял.
Объяснение никакое
0
Zitrix #
совсем ничерта?
допустим, у нас очень полезная фукнция f1, которую надо вызвать с некоторыми аргументами, например, при onclick'е на какую-нибудь кнопку. так и пишем: node.onclick = function() { f1(1,2,3); } — через какое-то время эту же функцию с такими же аргументами надо запускать, например, при инициализации страницы. у нас будет дважды повторяться f1(1,2,3);.
далее надо будет заменить 1,2,3 на 1,2,3,4: меняем аргументы в двух местах, что не приятно.

можно же просто записать myCallback = _clb(f1, [1,2,3]);, прописать и на onclick, и на инициализацию myCallback.fire — когда потребуется изменить список аргументов, сделать это можно будет в одном месте.

и чем больше в скрипте колбеков, тем больше толку от использования.
0
PsychodelEKS #
Приучите себя пихать в функцию не кучу параметров, а объект параметров, это решит большинство проблем с совместимостью/расширяемостью.
0
Zitrix #
то бишь, вместо нескольких аргументов один? ничего против не имею, но это дело вкуса.
но даже если аргумент всего один, то замыкания каждый раз все равно надо создавать — не удобно.
+2
homm #
Вы о замыканиях когданибудь слышали?
0
Zitrix #
оно и создается, только в одном месте (при создании объекта) независимо от кол-ва мест передачи колбека. плюс синтаксис много легче и читаемей.
+5
homm #
function make_callback(a, b, c) {
	return function() {
		return a + b + c;
	}
}

callback = make_callback(2, 3, 5);

alert(callback());

Или я чего-то не понимаю?
0
Zitrix #
все правильно, только меня return function() { запарило уже писать (как и инлайном function в скобки оборачивать)
0
slik #
var cb = function(fn, args, bind){
return function(){
return fn.apply(bind || window, args || []);
}
}

callback = cb(function(a){console.log(a)}, ['a']);
callback();
+1
Zitrix #
оно самое. если пойти дальше немного, то получится то, что написано в статье.
0
kurokikaze #
Можно передавать необходимые вещи аргументами. Но всё равно странная штука.
+2
forgotten #
Почему изобретатели всевозможных мертворожденных комбайнов избирают своей целью именно JavaSсript?
+1
Zitrix #
кто что знает, тот то и избирает
+3
webdew #
я полагаю forgotten хотел сказать, что не надо извращать лаконичный и красивый javascript
+4
webdew #
Пардон, я тоже ничего не понял.

Вы изнасиловали функцию bind?

Function.prototype.bind = function(scope) {
var f = this;

return function() {
return f.apply(scope, arguments);
}
}

для варианта с аргументами

Function.prototype.bind = function(scope)
{
var f=this;
if(arguments.length==1) return function(){return f.apply(scope,arguments)};

var fnSlice=Array.prototype.slice,a=fnSlice.call(arguments,1);
return function(){return f.apply(scope,a.concat(fnSlice.call(arguments)))}
};
+1
Zitrix #
похоже на то :)
+14
webdew #
Статья 131 УК РФ. Изнасилование — от трех до шести лет.
+7
shai_hulud #
С конфискацией орудия преступления
0
Richard_Ferlow #
Демо бы… а то что же изучать чего у вас там, чтобы возможно в результате понять что оно и не нужно вообще ни разу)))
0
Zitrix #
рядом с каждой строчкой результат закомментирован. и прямо над Вашим комментарием webdew указал куда более короткий листинг, пусть и с урезанными возможностями.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.