Но в один прекрасный день… Правильно! Прилетает мыло «ваша программа не работает» ((С) 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 строки кода, мы решили весьма редкую, но довольно пакостную проблему с зацикливанием событий. Кто знает (я вот не знаю), может и вам оно пригодится. Все тут.
Счастья, добра и электричества в ваши дома.
Комментарии (13)
vintage
24.10.2016 13:39+4Исключение не обязательно бросать, чтобы получить из него стектрейс.
arvitaly
24.10.2016 17:40Вы об этом?
https://github.com/v8/v8/wiki/Stack-Trace-API#customizing-stack-tracesvintage
24.10.2016 18:12Нет, об этом:
new Error().stack
alibertino
24.10.2016 18:21Ну да, но для получения подробной информации о каждом элементе стека, нужно заменить Error.prepareStackTrace на свою функцию, иначе тут будет уже конкатенированная строка.
mayorovp
24.10.2016 14:40+6У меня получилось проще: https://github.com/mayorovp/tiny-safeevents/blob/master/safeevents.js
Вместо ловли стека через исключение я в явном виде отслеживаю этот стек во внутреннем массиве.
AlexWriter
24.10.2016 21:20Может я что-то делаю не так, но ваше решение не работает (будьте осторожны JS уйдет в глубокий нимб после запуска).
У меня было решение без именных функций сродни вашему, но там возникает проблема иного рода, а именно нужно чуть более усердно думать об очистке за собой. Поэтому решение с именными функциями мне показалось наиболее оптимальным, так после себя ничего не оставляет. С этим примером можно в этом убедиться.mayorovp
24.10.2016 21:23Это второй тест, который я проверял, он работает. Да, у меня по умолчанию циклы только детектятся — но не разрываются. Надо подписаться на
'onloop'
и сделатьthrow
, чтобы разорвать цикл.
Связано это с тем, что не вполне понятно как один обработчик в подобной системе сможет отменить другой — поэтому я не стал добавлять заведомо деструктивных действий в обработчик по умолчанию.
AlexWriter
24.10.2016 22:07Не думаю, что событие должно порождать событие (то есть само себя). Из этой парадигмы мы исходили, да и исходим в проектировании. Если же возникнет такая ситуация, то лучше работать с такой ситуацией, как со специальным случаем, а не наоборот. Поэтому дефалтное поведение – остановить, а возможность «вырваться» из проверки остается через «небезопасный» асинхронный вызов.
Так же ваше решение отличается еще тем, что переопределяется ряд нативных средств (setTimeout, setInterval и другие), у нас это запрещено религией), хотя по большому счету к делу это и не относится. Но в целом мне нравится. Спасибо за ваше время на эксперименты.
pan-alexey
24.10.2016 21:43У меня возникает вопрос больше к использованию indexOf. Каюсь — не вникал глубоко в код, но допустим у меня вызывается событие Aa, а после него вызываю событие a — то выходит что условие indexOf отработает и будет исключение?
Odrin
Вы замеряли, как данное решение повлияло на производительность?
AlexWriter
Каких-то специальных тестов я не делал. Не совсем ясно, что тут может понизить производительность, тем более в жизни события не запускаются один за другим так «плотно». По тому проекту, где было применено данное решение никакого ухудшения замечено не было. Все-таки – это больше реакция на какое-то внешнее воздействие, а не поток событий.
С другой стороны, я делал тест на глубину цепочки, это да. Выложил на github в examples если интересно. Прогонка на 1000 событий, где 1000ое событие взывает нулевое, образуя цикл. Главным образом делалась проверка на утечки, но и производительность с этого теста тоже видна.