2010-03-02 2 views
123

Это может быть связано с тем, что обработчики событий jQuery всегда выполняются в том порядке, в котором они были связаны. Например:Обработчики событий jQuery всегда выполняются так, чтобы они были связаны - каким-либо образом обойти это?

$('span').click(doStuff1); 
$('span').click(doStuff2); 

нажав на пролете приведет doStuff1() к огню, а затем doStuff2().

В то время я связать doStuff2(), я хотел бы возможность связать его перед тем doStuff1(), но не кажется, что любой простой способ сделать это.

Я полагаю, что большинство людей сказали бы, просто написать код так:

$('span').click(function(){ 
    doStuff2(); 
    doStuff1(); 
}); 

Но это всего лишь простой пример - на практике это не всегда convienient, чтобы сделать это.

Бывают ситуации, когда вы хотите связать событие, а объект, к которому вы привязываетесь, уже имеет события. И в этом случае вы можете просто хотеть, чтобы новое событие срабатывало перед любыми другими существующими событиями.

Итак, каков наилучший способ достичь этого в jQuery?

+0

Вы только что сказали, что когда вы связываете 2-й обработчик событий, вы хотите, чтобы он был вызван до 1-го? –

+0

Можете ли вы привести пример, когда использовать второй пример не удобно? – Russell

+0

@jarret: Да, точно – asgeo1

ответ

110

Обновлено Ответ

JQuery изменил местоположение, где события хранятся в 1.8. Теперь вы знаете, почему это такая плохая идея, чтобы возиться с внутренним APIs :)

Нового внутреннего API для доступа к событиям для объекта DOM доступна через глобальный объект JQuery, и не привязана к каждый экземпляр, и он принимает элемент DOM в качестве первого параметра и ключ («события» для нас) в качестве второго параметра.

jQuery._data(<DOM element>, "events"); 

Так вот, измененный код для jQuery 1.8.

// [name] is the name of the event "click", "mouseover", .. 
// same as you'd pass it to bind() 
// [fn] is the handler function 
$.fn.bindFirst = function(name, fn) { 
    // bind as you normally would 
    // don't want to miss out on any jQuery magic 
    this.on(name, fn); 

    // Thanks to a comment by @Martin, adding support for 
    // namespaced events too. 
    this.each(function() { 
     var handlers = $._data(this, 'events')[name.split('.')[0]]; 
     // take out the handler we just inserted from the end 
     var handler = handlers.pop(); 
     // move it at the beginning 
     handlers.splice(0, 0, handler); 
    }); 
}; 

И вот playground.


Оригинал ответ

Как обнаружил @Sean, JQuery предоставляет все обработчик событий через интерфейс элемента data. Конкретно element.data('events'). Используя это, вы всегда можете написать простой плагин, в котором вы можете вставить любой обработчик событий в определенную позицию.

Вот простой плагин, который делает именно это, чтобы вставить обработчик в начале списка. Вы можете легко расширить его, чтобы вставить элемент в любую позицию. Это просто манипуляция с массивами. Но поскольку я не видел источник jQuery и не хочу пропустить какую-либо магию jQuery, я обычно добавляю обработчик, используя сначала bind, а затем перетаскиваю массив.

// [name] is the name of the event "click", "mouseover", .. 
// same as you'd pass it to bind() 
// [fn] is the handler function 
$.fn.bindFirst = function(name, fn) { 
    // bind as you normally would 
    // don't want to miss out on any jQuery magic 
    this.bind(name, fn); 

    // Thanks to a comment by @Martin, adding support for 
    // namespaced events too. 
    var handlers = this.data('events')[name.split('.')[0]]; 
    // take out the handler we just inserted from the end 
    var handler = handlers.pop(); 
    // move it at the beginning 
    handlers.splice(0, 0, handler); 
}; 

Так, например, для этой разметки она будет работать (example here):

<div id="me">..</div> 

$("#me").click(function() { alert("1"); }); 
$("#me").click(function() { alert("2"); });  
$("#me").bindFirst('click', function() { alert("3"); }); 

$("#me").click(); // alerts - 3, then 1, then 2 

Однако, поскольку .data('events') не является частью их публичного API, насколько я знаю, обновление jQuery может сломать ваш код, если базовое представление вложенных событий, например, изменяется от массива к чему-то другому.

Отказ от ответственности: поскольку все возможно :), вот ваше решение, но я все равно ошибаюсь на стороне рефакторинга существующего кода, так как просто попытка запомнить порядок, в котором эти элементы были прикреплены, скоро может выйти из когда вы продолжаете добавлять все больше и больше этих упорядоченных событий.

+4

Вы правы, если .data() не является частью API, это риск его использования. Тем не менее, мне нравится ваше предложение инкапсулировать использование его в новую функцию jQuery. По крайней мере, тогда, если .data() разбивается на более позднюю версию, вам нужно обновить только одну функцию – asgeo1

+2

. Таким образом, изменения будут изолированы от одной функции. – Anurag

+0

http://api.jquery.com/jQuery.data/ –

1

Я предполагаю, что вы говорите об этом пузыречном аспекте события. Было бы полезно увидеть ваш HTML для указанных span элементов. Я не понимаю, почему вы хотите изменить основное поведение, как это, я не считаю это совершенно раздражающим. Я предлагаю идти со своим вторым блоком кода:

$('span').click(function(){ 
    doStuff2(); 
    doStuff1(); 
}); 

Самое главное, я думаю, вы найдете его более организованным, если вам удастся все события для данного элемента в том же блоке, как вы показано на рисунке. Можете ли вы объяснить, почему вы это раздражаете?

+0

Это был просто пример, иллюстрирующий проблему. Мой фактический случай более сложный, так что для перезаписи кода в этой форме потребуется некоторая работа. В некоторых ситуациях я даже могу использовать стороннюю библиотеку и, возможно, не хочу ее модифицировать. Я просто думаю, что было бы более удобно иметь возможность добавления события в начало * или * в конец стека событий. Если в jQuery есть способ сделать это, мне будет интересно узнать. – asgeo1

33

Вы можете создать собственное пространство имен событий.

$('span').bind('click.doStuff1',function(){doStuff1();}); 
$('span').bind('click.doStuff2',function(){doStuff2();}); 

Затем, когда вам нужно вызвать их, вы можете выбрать заказ.

$('span').trigger('click.doStuff1').trigger('click.doStuff2'); 

или

$('span').trigger('click.doStuff2').trigger('click.doStuff1'); 

Кроме того, только вызвав щелчок должен вызывать как в порядке, они были связаны ... так что вы все еще можете сделать

$('span').trigger('click'); 
+1

+1 Вы каждый день узнаете что-то новое. Спасибо, Рассел! –

+0

Спасибо, помощник. Я также не знал о пользовательских пространствах имен. Я определенно смогу использовать эту технику в будущем. Я дал правильный ответ Анурагу, потому что он был ближе к тому, чего я пытался добиться в этом обстоятельстве. – asgeo1

4

Стандартный принцип отдельное событие обработчики не должны зависеть от порядка, который они вызывают. Если они зависят от заказа, они не должны быть раздельными.

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

12

Очень хороший вопрос ... Я был заинтригован, поэтому немного поработал; для тех, кто заинтересован, вот куда я пошел и что я придумал.

Глядя на исходный код для jQuery 1.4.2 Я видел этот блок между строками 2361 и 2392:

jQuery.each(["bind", "one"], function(i, name) { 
    jQuery.fn[ name ] = function(type, data, fn) { 
     // Handle object literals 
     if (typeof type === "object") { 
      for (var key in type) { 
       this[ name ](key, data, type[key], fn); 
      } 
      return this; 
     } 

     if (jQuery.isFunction(data)) { 
      fn = data; 
      data = undefined; 
     } 

     var handler = name === "one" ? jQuery.proxy(fn, function(event) { 
      jQuery(this).unbind(event, handler); 
      return fn.apply(this, arguments); 
     }) : fn; 

     if (type === "unload" && name !== "one") { 
      this.one(type, data, fn); 

     } else { 
      for (var i = 0, l = this.length; i < l; i++) { 
       jQuery.event.add(this[i], type, handler, data); 
      } 
     } 

     return this; 
    }; 
}); 

Существует много интересные вещи здесь происходят, но часть мы заинтересованы в том, между линиями 2384 и 2388:

else { 
    for (var i = 0, l = this.length; i < l; i++) { 
     jQuery.event.add(this[i], type, handler, data); 
    } 
} 

Каждый раз, когда мы называем bind() или one() мы на самом деле делает вызов jQuery.event.add() ... так что давайте посмотрим на что (линии 1557 до 1672, если вы заинтересованы)

add: function(elem, types, handler, data) { 
// ... snip ... 
     var handleObjIn, handleObj; 

     if (handler.handler) { 
      handleObjIn = handler; 
      handler = handleObjIn.handler; 
     } 

// ... snip ... 

     // Init the element's event structure 
     var elemData = jQuery.data(elem); 

// ... snip ... 

     var events = elemData.events = elemData.events || {}, 
      eventHandle = elemData.handle, eventHandle; 

     if (!eventHandle) { 
      elemData.handle = eventHandle = function() { 
       // Handle the second event of a trigger and when 
       // an event is called after a page has unloaded 
       return typeof jQuery !== "undefined" && !jQuery.event.triggered ? 
        jQuery.event.handle.apply(eventHandle.elem, arguments) : 
        undefined; 
      }; 
     } 

// ... snip ... 

     // Handle multiple events separated by a space 
     // jQuery(...).bind("mouseover mouseout", fn); 
     types = types.split(" "); 

     var type, i = 0, namespaces; 

     while ((type = types[ i++ ])) { 
      handleObj = handleObjIn ? 
       jQuery.extend({}, handleObjIn) : 
       { handler: handler, data: data }; 

      // Namespaced event handlers 
        ^
        | 
     // There is is! Even marked with a nice handy comment so you couldn't miss it 
     // (Unless of course you are not looking for it ... as I wasn't) 

      if (type.indexOf(".") > -1) { 
       namespaces = type.split("."); 
       type = namespaces.shift(); 
       handleObj.namespace = namespaces.slice(0).sort().join("."); 

      } else { 
       namespaces = []; 
       handleObj.namespace = ""; 
      } 

      handleObj.type = type; 
      handleObj.guid = handler.guid; 

      // Get the current list of functions bound to this event 
      var handlers = events[ type ], 
       special = jQuery.event.special[ type ] || {}; 

      // Init the event handler queue 
      if (!handlers) { 
       handlers = events[ type ] = []; 

        // ... snip ... 

      } 

        // ... snip ... 

      // Add the function to the element's handler list 
      handlers.push(handleObj); 

      // Keep track of which events have been used, for global triggering 
      jQuery.event.global[ type ] = true; 
     } 

    // ... snip ... 
    } 

В этот момент я понял, что понимание этого собирается занять более 30 минут ... так что я искал Stackoverflow для

jquery get a list of all event handlers bound to an element 

и нашел this answer для Перебор связанных событий:

//log them to the console (firebug, ie8) 
console.dir($('#someElementId').data('events')); 

//or iterate them 
jQuery.each($('#someElementId').data('events'), function(i, event){ 

    jQuery.each(event, function(i, handler){ 

     console.log(handler.toString()); 

    }); 

}); 

Тестирование того, что в Firefox я вижу, что объект events в атрибуте data каждого элемента имеет атрибут [some_event_name] (click в нашем случае), к которому прикреплен массив из handler, каждый из которых имеет направляющую, пространство имен, тип и обработчик. «Итак, я думаю,« мы должны теоретически иметь возможность добавлять объекты, построенные таким же образом, к [element].data.events.[some_event_name].push([our_handler_object); ... »

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

Это доказательство того, что Stackoverflow - лучший вопрос, и-ответ на сайте в Интернете, по крайней мере, по моему скромному мнению.

Итак, я отправляю это ради потомства ... и отмечаю его как сообщество wiki, так как RussellUresti уже так хорошо ответил на вопрос.

+1

+1 Спасибо за весь ваш анализ. Шон. Очень полезно – asgeo1

1

Вот решения для JQuery 1.4.x (к сожалению, принятый ответ не работает для JQuery 1.4.1)

$.fn.bindFirst = function(name, fn) { 
    // bind as you normally would 
    // don't want to miss out on any jQuery magic 
    this.bind(name, fn); 

    // Thanks to a comment by @Martin, adding support for 
    // namespaced events too. 
    var handlers = this.data('events')[name.split('.')[0]]; 
    // take out the handler we just inserted from the end 
    var copy = {1: null}; 

    var last = 0, lastValue = null; 
    $.each(handlers, function(name, value) { 
     //console.log(name + ": " + value); 
     var isNumber = !isNaN(name); 
     if(isNumber) {last = name; lastValue = value;}; 

     var key = isNumber ? (parseInt(name) + 1) : name; 
     copy[key] = value; 
    }); 
    copy[1] = lastValue; 
    this.data('events')[name.split('.')[0]] = copy; 
}; 
4

.data ("событие") был удален в версии 1.9 и 2.0beta, поэтому вы не можете больше полагаться на эти решения.

http://jquery.com/upgrade-guide/1.9/#data-quot-events-quot-

+1

Ответ был обновлен, и вот скрипка, показывающая, что это работает в jquery 2.edge http://jsfiddle.net/g30rg3/x8Na8/171/ – George

3

Для JQuery 1.9+ как Dunstkreis упомянутые .data ('событий') был удален. Но вы можете использовать другой хак (не рекомендуется использовать недокументированные возможности) $ ._ данных ($ (это) .get (0), «события»), а и решение, предоставляемое anurag будет выглядеть следующим образом:

$.fn.bindFirst = function(name, fn) { 
    this.bind(name, fn); 
    var handlers = $._data($(this).get(0), 'events')[name.split('.')[0]]; 
    var handler = handlers.pop(); 
    handlers.splice(0, 0, handler); 
}; 
+0

Я использовал делегат, несмотря на 'bind' для делегирования событий. –

1

Совет Криса Чилверса должен быть первым курсом действия, но иногда мы имеем дело с библиотеками сторонних разработчиков, которые делают это сложным и требуют от нас совершать непослушные вещи ... что это такое. ИМО это преступление презумпции, похожее на использование! Важно в CSS.

Сказав, что, основываясь на ответе Анурага, вот несколько дополнений. Эти методы допускают множественные события (например, «keydown keyup paste»), произвольное позиционирование обработчика и переупорядочение после факта.

$.fn.bindFirst = function (name, fn) { 
    this.bindNth(name, fn, 0); 
} 

$.fn.bindNth(name, fn, index) { 
    // Bind event normally. 
    this.bind(name, fn); 
    // Move to nth position. 
    this.changeEventOrder(name, index); 
}; 

$.fn.changeEventOrder = function (names, newIndex) { 
    var that = this; 
    // Allow for multiple events. 
    $.each(names.split(' '), function (idx, name) { 
     that.each(function() { 
      var handlers = $._data(this, 'events')[name.split('.')[0]]; 
      // Validate requested position. 
      newIndex = Math.min(newIndex, handlers.length - 1); 
      handlers.splice(newIndex, 0, handlers.pop()); 
     }); 
    }); 
}; 

Можно экстраполировать на это с методами, которые бы разместить данный обработчик до или после некоторого другого данного обработчика.

3

Выбранный ответ, созданный Анурагом, является лишь частично правильным. Из-за некоторых внутренних функций обработки событий jQuery предлагаемая функция bindFirst не будет работать, если у вас есть сочетание обработчиков с фильтрами и без них (например: $ (document) .on («click», handler) vs $ (document) .on («клик», «кнопка», обработчик)).

Проблема заключается в том, что jQuery поместит (и ожидает), что первые элементы массива обработчика будут такими обработанными фильтрами, поэтому размещение нашего события без фильтра в начале разрывает эту логику, и все начинает разваливаться. Обновленная функция bindFirst должна быть следующей:

$.fn.bindFirst = function (name, fn) { 
    // bind as you normally would 
    // don't want to miss out on any jQuery magic 
    this.on(name, fn); 

    // Thanks to a comment by @Martin, adding support for 
    // namespaced events too. 
    this.each(function() { 
     var handlers = $._data(this, 'events')[name.split('.')[0]]; 
     // take out the handler we just inserted from the end 
     var handler = handlers.pop(); 
     // get the index of the first handler without a selector 
     var firstNonDelegate = handlers.first(function(h) { return !h.selector; }); 
     var index = firstNonDelegate ? handlers.indexOf(firstNonDelegate) 
            : handlers.length; // Either all handlers are selectors or we have no handlers 
     // move it at the beginning 
     handlers.splice(index, 0, handler); 
    }); 
}; 
+1

Мне было интересно об этом! Благодарю. Проблема только в том, что эта версия не принимает аргумент для делегированных событий. Я хотел бы иметь возможность вызывать $ .fn.bindFirst (имя, селектор, обработчик). Я попытаюсь понять, как, но, поскольку вы, кажется, понимаете это лучше меня, я уверен, что сообщество выиграет от другой ревизии. – fronzee

+1

Это должно быть очень похоже, хотя вам нужно было бы перетасовать аргумент перед вызовом 'this.on (name, fn);'. В основном добавьте параметр для селектора в подпись, а затем сразу же проверьте, является ли селектор функцией или строкой. Если селектор является функцией, установите fn = селектор и селектор = null. Наконец, в этом случае вам нужно будет изменить логику для вставки в первом не-делегате, если селектор имеет значение null или 0, если селектор не равен нулю. – dshapiro

Смежные вопросы