42 строки кода для выхода из лимба
Но в один прекрасный день… Правильно! Прилетает мыло «ваша программа не работает» (© bash), телефоны разогреваются до красна, а юристы нервно перечитывают, что они там накидали в раздел «гарантийное обслуживание».
Ровно такая ситуация была и у нас. Делали мы довольно увесистый проект, суть которого можно было бы описать так (кратко, конечно): есть разный контент (клиентская база, маркетинговая база, база связей и прочее, прочее, прочее) и различные способы его представления (widget, popup, modal etc.). Иными словами, с нашей стороны была подготовлена платформа (API доступа к данным, визуализация, вся, как это модно говорить, экосистема (хотя я не знаю, что это значит, но звучит уж очень круто)), чтобы разработчики заказчика могли писать свои контролеры данных и просто файликом их «класть» в указанное место, после чего счастливо лицезреть, как появляется новенький виджет со списком текущих котировок по какому-нибудь мудрёному индексу.
И как я уже сказал, все складывалось хорошо. Провели несколько «мастер» классов, все показали, все рассказали, выпили пива и завертелось. Уже без нас.
Пока все не сломалось. Именно так: «все» и «сломалось». В какие-то моменты приложение просто стало намертво виснуть. Да так, что вкладку браузера не закроешь. Мало-мальски опытный web-developer тут же скажет — у вас цикл где-то заклинило ребятишки. И будет прав, что уж там.
Но прежде чем перейти к тем самым 42 строкам кода, я лишь напомню, что когда имеется приложение с кучей всяких вкусняшек внутри, то лучший способ организации коммуникации между ними — это внутренние события. И у нашего в момент сдачи проекта их было под сотню, а когда начались проблемы их число приросло еще на пару десятков.
И, как вы уже догадались, «клинило» как раз контроллер событий. На пальцах: событие A, вызывает событие B, а событие B — событие C, а оно, в свою очередь, вновь вызывает событие A. Та-да-м, встречайте цикл!
Наш обработчик событий был до безобразия простой и ютился в файлике на 44 строках кода. Однако он не умел делать весьма актуальную вещицу — проверять не в цикле ли он.
Много пить думать не пришлось и решение нашли довольно быстро. Хорошее оно или плохое — это все на ваш суд. Опишу лишь основную идею.
Единственный способ проверить «кто» вызвал цепочку событий (в нашем примере найти A, B и C) — это проверить stack. Чтобы получить stack нужно просто «выбросить» ошибку.
Остается проблема — как «пометить» место вызова события, ведь в стеке должно быть что-то, что помогло бы распознать всю цепочку ранее запущенных событий? Одно из решений этой проблемы — поименованные функции-обертки, в имени которых как раз и хранится вся информация о предыдущих событиях. Ничего не поняли? Я вот написал, перечитал и тоже не понял. Проще посмотреть код.
В общем теперь, если выполнить это (событие A вызывает B, B вызывает С, а C вновь вызывает A):
var safeevents = new SafeEvents();
safeevents.bind('A', function () {
safeevents.trigger('B');
});
safeevents.bind('B', function () {
safeevents.trigger('C');
});
safeevents.bind('C', function () {
safeevents.trigger('A');
});
safeevents.trigger('A');
То на этот раз приложение не уйдет в лимб, а выбросит в консоль исключение «Uncaught Error: Event [A] called itself. Full chain: A, B, C». Profit. Теперь разработчику не нужно уходить на три дополнительных перекура, чтобы сообразить в чем собственно дело — все видно из сообщения в консоли.
С асинхронными вызовами немного сложнее. Для них, увы, нужно выполнять дополнительное действие. Но оно настолько крохотное, что вряд ли будет большой проблемой.
var safeevents = new SafeEvents();
safeevents.bind('A', function () {
safeevents.trigger('B');
});
safeevents.bind('B', function () {
safeevents.trigger('C');
});
safeevents.bind('C', function () {
/*
* Use method "safely" to wrap your async methods and create safe callback.
*/
setTimeout(safeevents.safely(function () {
safeevents.trigger('A');
}), 10);
});
safeevents.trigger('A');
Обратите внимание на функцию обратного вызова в таймере. Мы добавляем «обертку», чтобы передать данные о предыдущих событиях в асинхронных вызовах. И вновь в консоли мы увидим: «Uncaught Error: Event [A] called itself. Full chain: A, B, C».
Конечно, не всегда нужно брать так и нагло выбрасывать исключение. Значительно лучше тихонечко сообщить куда следует записать данные в лог или отправить уведомление админу. Для чего можно поставить свой обработчик на случай «зацикливания» и получить все необходимые данные.
var safeevents = new SafeEvents();
safeevents.bind('A', function () {
safeevents.trigger('B');
});
safeevents.bind('B', function () {
safeevents.trigger('C');
});
safeevents.bind('C', function () {
safeevents.trigger('A');
});
safeevents.bind(safeevents.onloop, function (e, chain, last_event, stack) {
console.log('Error message: ' + e);
console.log('Full chain of events: ' + chain.join(', '));
console.log('Last event (generated loop): ' + last_event);
console.log('Error stack: ' + stack);
});
safeevents.trigger('A');
Теперь наше приложение вовсе не прерывается, но цикл при этом будет успешно предотвращен.
В общем переписав наш наипростейший обработчик событий и получив дополнительно 42 строки кода, мы решили весьма редкую, но довольно пакостную проблему с зацикливанием событий. Кто знает (я вот не знаю), может и вам оно пригодится. Все тут.
Счастья, добра и электричества в ваши дома.