Устойчивость системы обработчиков к ошибкам

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

Если какой-нибудь обработчик содержит ошибку, то, генерируя исключение, он ломает цикл вызова остальных обработчиков текущего элемента/события.

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

Копить ошибки

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

Что делать дальше?

Есть варианты.. Например, библиотека Yahoo UI хранит последнее пойманное исключение в специальной переменной lastError. А фреймворк Mochikit собирает все пойманные исключения в errors.

В коде ниже проиллюстрирован подход из Mochikit:

function commonHandle(event) {
  event = fixEvent(event)
  handlers = this.events[event.type]
  
  // (1)
  var errors = []
 
  for ( var g in handlers ) {
    try {
      var ret =  handlers[g].call(this, event)
      if ( ret === false ) {
        event.preventDefault()
        event.stopPropagation()
      }
    } catch(e) {
      // (2)
      errors.push(e)
    }
  }
  
  // (3)
  if (errors.length == 1) {
      throw errors[0]
  } else if (errors.length > 1) {
      var e = new Error("Multiple errors thrown in handling 'sig', see errors property");
      e.errors = errors
      throw e
  }  
}
  1. Инициализуется массив errors для исключений
  2. Все исключения - добавляем в массив, при этом не прерывая цепочку обработчиков.
  3. Если была только одна ошибка - кидаем ее дальше. Иначе делаем общий объект Error со списком ошибок в свойстве errors и кидаем его.

Вызывать обработчики через setTimeout

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

// вместо
handlers[g].call(this, event)
// поставить
setTimeout(function() { handlers[g].call(..) }, 0)

При этом - да, обработчики будут вызваны независимо. Но к моменту, когда сработает setTimeout - событие будет уже "мертво", нельзя будет ни вызвать preventDefault, ни отменить всплытие.

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

Поэтому, увы, этот способ никак не годится.

Создание своего контекста исполнения

Этот подход описан Dean Edwards в статье Callbacks and Events

Он заключается в том, что каждое событие заворачивается не в setTimeout, а в dispatchEvent (fireEvent для IE).

Таким образом, события вызываются в независимых потоках выполнения и инициируют ошибки независимо.

Резюме

Есть несколько вариантов, как сделать систему событий устойчивой к ошибкам.
Жизнеспособны - первый подход (YUI/Mochikit) и последний (Dean).

Их можно с успехом использовать на практике.