Конечные автоматы в JavaScript. Часть 2: Реализация виджета (исходники)

Эдвард Принг, старший инженер-программист, IBM

Конечные автоматы являются организующим принципом для тех разработчиков, которые проектируют и реализуют сложное поведение в программах, управляемых событиями, например, в сетевых адаптерах и компиляторах. А теперь программируемые web -браузеры открыли новую управляемую событиями среду для нового поколения приложений. Поскольку приложения на базе браузера, популярность которых возросла с появлением технологий Ajax, стали более сложными, вы можете получить преимущество, если воспользуетесь дисциплиной разработки и структурой, которые предлагают конечные автоматы.

В части 1 рассказывается о виджете подсказки для web-страниц; такие подсказки демонстрируют более совершенное поведение, чем встроенные подсказки, предоставляемые популярными web-браузерами. Этот виджет FadingTooltip постепенно выводит отображение подсказки в поле зрения после того, как курсор остановится над HTML-элементом, а затем плавно удаляет ее после того, как она некоторое время побудет на экране. Подсказка следует за перемещениями курсора даже в те моменты, когда ее видимость постепенно нарастает или снижается, а эффект пропадания/появления изменяет направление, когда курсор смещается с HTML-элемента или возвращается в его пределы. Такое поведение требует от виджета FadingTooltip реакции на множество различных событий, а в некоторых случаях определенной реакции на конкретное событие в зависимости от предыдущего события.

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

Рисунок 1. Таблица состояний виджета FadingTooltip
Таблица состояний для виджета FadingTooltip

Строки и столбцы таблицы состояний озаглавлены именами событий, на которые будет реагировать виджет, и именами состояний, в которых виджет будет ожидать между событиями. Каждая ячейка таблицы определяет действия, которые выполнит виджет при наступлении конкретного события в конкретном состоянии. В ячейках таблицы можно также задать следующее состояние, в которое перейдет виджет после выполнения действия, или определить, что виджет останется в прежнем состоянии. Пустые ячейки таблицы показывают, что в данных состояниях данные события не наступают. Кроме того, в части 1 мы составили список переменных состояния, которые виджет должен запоминать между событиями, чтобы он мог выполнять связанные действия, указанные в различных ячейках.

 
В части 3 мы выполним тестирование реализации виджета в популярных браузерах и рассмотрим на практике ситуации, в которых событие "не должно наступить".
 

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

Перевод проекта на JavaScript

Закончив разработку проекта конечного автомата в части 1, мы подготовились к реализации виджета FadingTooltip на языке JavaScript. Именно здесь от простых абстракций на этапе проектирования разработчик переходит к суровой действительности реальной среды выполнения.

Необходимо выбирать только самые последние версии самых популярных браузеров: Netscape Navigator, Microsoft® Internet Explorer®, Opera и Mozilla Firefox. Даже этот ограниченный набор сред выполнения обеспечит вам достаточно проблем. Вы столкнетесь с деталями реального перехвата событий мыши и таймера в различных браузерах программой на JavaScript. Элегантная особенность языка JavaScript под названием замыкание функций придет к нам на выручку. Мы воспользуемся еще одной замечательной особенностью языка JavaScript, ассоциативными массивами , для перевода нашей таблицы состояний непосредственно в программный код. И, наконец, вы увидите, как создать подсказку и назначить ей определенный стиль при помощи HTML-элемента Division, добавить в нее текст и изображения, поместить рядом с курсором, плавно наращивая и ослабляя ее видимость, а также научить ее следовать за перемещениями курсора.

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

Объект, в котором будет размещаться все остальное

Виджет FadingTooltip потребует более сложных действий при программировании, чем обычные короткие фрагменты кода JavaScript, которые web-дизайнеры часто вырезают и вставляют в HTML-страницы. Инженерам по программному обеспечению будет комфортно работать с идеей группировки переменных и методов виджета в объекте, хотя объектная модель JavaScript может показаться немного необычной тем из них, кто изучал программирование на Java™ и С++. Объект JavaScript вполне удовлетворяет нашим требованиям: он позволяет сгруппировать переменные и методы в один объект, а затем создать отдельные экземпляры данных для каждой подсказки. Экземпляры объекта будут использовать общий код и выполняться независимо друг от друга.

В JavaScript объект конструктор - это просто функция; причем имя функции является именем объекта. Нашему виджету необходимо будет знать, в какой HTML-элемент встраиваться и какое содержимое отображать в подсказке для него, поэтому мы зададим эти аргументы в конструкторе и сохраним их в объекте. (Нам также потребуется способ задания параметров, связанных с поведением и отображением подсказки, поэтому мы также определим аргумент для этих целей и используем его далее в этой статье.) Переменные языка нетипизированные, поэтому конструктор объекта может начинаться с кода, показанного в листинге 1.

Листинг 1. Код JavaScript для конструктора объекта FadingTooltip

                
function FadingTooltip(htmlElement, tooltipContent, parameters) { 
    this.htmlElement = htmlElement; // сохраняет указатель на HTML -элемент, событие  
                                    // мыши для которого сцеплено с этим объектом
    this.tooltipContent = tooltipContent; // сохраняет текст и тэги для 
                                          // HTML-элемента Division 
    ...

В JavaScript можно добавлять свойства объекта, которые могут быть переменными или методами, к объекту либо при его создании, либо в любое время после этого, просто задав для них значения, поскольку конструктор выполняет для свойств методы this.htmlElement и this.tooltipContent.

В JavaScript прототип объекта - это шаблон для создания нового экземпляра объекта; он определяет исходные свойства объекта и их начальные значения. Начнем создавать прототип нашего объекта с переменных состояния, которые мы определили в части 1 данной серии и которые необходимы нашему виджету, как показано в листинге 2.

Листинг 2. Код JavaScript для прототипа объекта FadingTooltip

                
FadingTooltip.prototype = { 
    currentState: null,    // текущее состояние конечного автомата (одно из 
                           // имен состояний, приведенных в следующей таблице)
    currentTimer: null,    // значение, возвращаемое методом setTimeout, 
                           // не равно нулю, если выполняется таймер
    currentTicker: null,   // возвращается методом  setInterval, не равно нулю, 
                           // если выполняется тикер
    currentOpacity: 0.0,   // текущая непрозрачность подсказки, значение 
                           // от 0.0 до 1.0 включительно
    tooltipDivision: null, // указатель на  HTML-элемент division при видимой подсказке
    lastCursorX: 0,        // координата x-курсора в момент самого последнего события мыши
    lastCursorY: 0,        // координата y-курсора в момент самого последнего события мыши
    ...

Прототип объекта - хорошее место для определения практически любых свойств, имеющих отноошение к конечному автомату: таблицы состояний, ее действий и параметров действий. Осталось совсем немного -- выполнить привязку к событиям курсора -- чтобы закончить создание конструктора объекта, а остальная часть статьи будет посвящена работе с прототипом объекта.

Привязка (перехват) событий курсора

Как упоминалось при рассмотрении проектного этапа в части 1, браузеры могут передавать события коду JavaScript, когда курсор задерживается над, перемещается в пределах или смещается с HTML-элемента. Эти события содержат такую полезную информацию, как тип события и актуальная позиция курсора на странице. Браузеры передают события посредством вызова функций, которые были предварительно зарегистрированы. К сожалению, способы регистрации этих функций и передачи аргументов в браузерах различны. Чтобы обеспечить перехват событий курсора в конечном автомате во всех популярных браузерах, необходимо реализовать три различных модели событий. К счастью, программный код для каждой модели событий довольно компактный. К сожалению, компактность этого кода не оправдывает его сложности.

Mozilla Firefox, Opera и последние версии Netscape Navigator поддерживают стандартизированную модель событий, предложенную консорциумом World Wide Web (W3C). Это будет вашим первым выбором из-за простоты регистрации (и отмены регистрации) функций событий и из-за того, что сцепление нескольких зарегистрированных функций обрабатывается браузером. Если доступно, можно перехватывать события курсора посредством вызова метода addEventListener HTML-элемента, передавая тип события и информацию о том, какую функцию следует вызвать при наступлении этого события для данного HTML-элемента, как показано в листинге 3.

Листинг 3. Код JavaScript для перехвата событий курсора

                
function FadingTooltip(htmlElement, tooltipContent, parameters) { 
    ...
    htmlElement.fadingTooltip = this;
    if (htmlElement.addEventListener) { // for FF and NS and Opera
        htmlElement.addEventListener(
            'mouseover', 
            function(event) { this.fadingTooltip.handleEvent(event); }, 
            false);
        htmlElement.addEventListener(
            'mousemove', 
            function(event) { this.fadingTooltip.handleEvent(event); }, 
            false);
        htmlElement.addEventListener(
            'mouseout',  
            function(event) { this.fadingTooltip.handleEvent(event); }, 
            false);
    }
    ...

Вторым аргументом для вызовов addEventListener будут анонимные функции, то есть такие функции, которые не имеют имен. Это первая возможность определить функции в других функциях в языке JavaScript, но не последняя, поэтому давайте займемся ею сейчас. Функцию function можно использовать в любом месте кода JavaScript , чтобы задать описание анонимной функции по ходу работы. Эта функция возвращает указатель на функцию, которая впоследствии может использоваться как любое другое ссылочное значение. В нашем виджете FadingTooltip мы передадим указатель функции другой функции в качестве аргумента, проверим их на null, присвоим переменным и объявим в качестве методов объекта.

Похоже, что анонимные функции, передаваемые методу addEventListener , делают не слишком много. При наступлении события курсора браузер вызовет их, передавая объект event, который они, в свою очередь, передадут методу handleEvent объекта FadingTooltip. Объекты событий браузера содержат тип события и информацию о положении курсора, поэтому метод handleEvent может обрабатывать все события курсора, на которые должен реагировать виджет.

Эти несложные анонимные функции, кроме того, выполняют еще одну важную, но незаметную задачу. В модели события W3C функции, зарегистрированные при помощи метода addEventListener HTML-элемента, становятся методами этого элемента, поэтому при вызове их браузером встроенная переменная this указывает на этот HTML-элемент. Но метод handleEvent нуждается в указателе на объект FadingTooltip, который содержит наши переменные состояния. Один из способов указать ему этот объект - это добавить свойство fadingTooltip в HTML-элемент, которое укажет на объект FadingTooltip, а затем перейдет к нему, чтобы вызвать метод handleEvent нашего объекта. Благодаря этому, this указывает на объект FadingTooltip при выполнении метода handleEvent.

Перехват событий курсора в Internet Explorer

Microsoft Internet Explorer в настоящее время не поддерживает предложенную W3C стандартную модель событий, а предлагает собственную простую модель. К этим отличиям, перечисленным ниже, несложно приспособиться:

  • Несколько отличаются типы событий;
  • Зарегистрированные методы не становятся методами HTML-элемента;
  • Объекты событий остаются в глобальном объекте window.

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

Это ваша первая возможность использовать замыкания функций для включения переменных в определения функций, но также не последняя, поэтому давайте рассмотрим и этот вариант.

Листинг 4. Код JavaScript для перехвата событий курсора в Internet Explorer

                
function FadingTooltip(htmlElement, tooltipContent, parameters) { 
    ...
    else if (htmlElement.attachEvent) { // for MSIE 
        htmlElement.attachEvent(
            'onmouseover', 
            function() { htmlElement.fadingTooltip.handleEvent(window.event); } );
        htmlElement.attachEvent(
            'onmousemove', 
            function() { htmlElement.fadingTooltip.handleEvent(window.event); } );
        htmlElement.attachEvent(
            'onmouseout',  
            function() { htmlElement.fadingTooltip.handleEvent(window.event); } );
    } 
    ...

Функции, зарегистрированные при помощи метода attachEvent HTML-элемента, не становятся методом этого элемента. Когда наступит событие курсора, браузер вызовет этот метод, но встроенная переменная this укажет на глобальный объект window, а не на HTML-элемент, поэтому наши функции не смогут найти объект FadingTooltip по указателю, сохраненному в HTML-элементе.

К счастью, определения анонимной функции входят в лексический контекст аргумента htmlElement в конструкторе объекта. При помощи простого использования переменной htmlElement в описании анонимной функции можно включить его в такие функции. Это называется замыканием функции : если функция описана внутри другой функции, причем внутренняя функция использует локальные переменные внешней функции, то JavaScript сохраняет такие переменные вместе с описанием внутренней функции. В дальнейшем, после возвращения внешней функции, при вызове внутренней функции локальные переменные внешней функции будут ей по-прежнему доступны.

Здесь JavaScript сохранит значение переменной htmlElement после возврата конструктора, чтобы оно оставалось доступным анонимным функциям при вызове их браузером. Это позволит им найти свои HTML-элементы и перейти от указателей к объектам FadingTooltip без помощи браузера.

Поскольку замыкания функций являются особенностью языка JavaScript, они прекрасно работают в браузерах, которые используют модель событий W3C. Их можно было бы использовать для того, чтобы включить значение аргумента конструктора htmlElement в анонимные функции, описанные в предыдущем разделе, вместо того, чтобы использовать встроенную переменную this.

Перехват событий курсора в устаревших версиях браузеров

Для устаревших версий браузеров, которые не поддерживают модели событий W3C или Internet Explorer, придется выполнять перехват событий при помощи оригинальной модели событий, предлагаемой более ранними версиями браузера Netscape Navigator. Она поддерживается всеми популярными браузерами и широко используется web-дизайнерами для анимации web-страниц, но это самый нежелательный вариант для разработки более сложных приложений, поскольку эта модель не может выполнять сцепление нескольких обработчиков событий. Чтобы выполнить сам перехват, включите указатель на предварительно зарегистрированные функции события в описание ваших собственных функций событий, а затем вызовите их после вызова метода handleEvent, как показано в листинге 5.

Листинг 5. Код JavaScript для перехвата событий курсора в старых браузерах

                
function FadingTooltip(htmlElement, tooltipContent, parameters) { 
    ...
    else { // for older browsers
        var self = this;
        var previousOnmouseover = htmlElement.onmouseover;
        htmlElement.onmouseover = function(event) { 
            self.handleEvent(event ? event : window.event); 
            if (previousOnmouseover) { 
                htmlElement.previousHandler = previousOnmouseover;
                htmlElement.previousHandler(event ? event : window.event); 
            }
        };
         ... and similarly for 'onmousemove' and 'onmouseout' ... 
    }
}

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

Для разнообразия, этот код копирует переменную конструктора this, которая указывает на объект FadingTooltip, в локальную переменную с именем self, а затем использует указатель self, чтобы найти объект FadingTooltip в описании анонимной функции. Таким образом, указатель на объект FadingTooltip включается в описание анонимной функции, чтобы его можно было найти непосредственно при вызове функций браузером, независимо от предоставления указателя на HTML-элемент браузером и без необходимости сохранять указатель на объект FadingTooltip в HTML-элементе.

Можно также включить указатель на объект FadingTooltip в анонимные функции, которые вы описали для моделей событий W3C и Microsoft. Благодаря этому отпадает необходимость в сохранении указателей на наши объекты в HTML-элементах, а для нахождения HTML-элемента во всех моделях событиях применяется один и тот же метод. Конструктор в данном исходном коде запрограммирован по этому же принципу.

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

Настройка таймеров и перехват событий таймеров

Мы закончили работу над конструктором FadingTooltip и можем продолжить работу над прототипом. В JavaScript прототип объекта может включать методы и переменные; методы - это обычные переменные, которые указывают на функции. Мы начнем с некоторых методов общего назначения, которые запускают и отменяют таймеры.

Возможно, вы помните из первой части данной серии статей, что JavaScript предоставляет два типа таймеров, разовые таймеры и тикеры, причем конечному автомату необходимы оба типа. Вы можете запустить таймер, вызвав функцию setTimeout или setInterval, передав значение времени (в миллисекундах), при этом функция будет вызвана , соответственно, по истечении времени таймера или наступлении события timetick. Эти функции возвратят глухие ссылки, которые затем можно передать функции clearTimeout или clearInterval для отмены таймера.

Браузеры вызовут функции событий таймера, которые были переданы как аргументы функции setTimeout и setInterval , сразу по истечении значения timeout, или многократно при каждом наступлении интервала timetick, соответственно, пока эти таймеры не будут отменены. Но эти функции timeout и timetick не получают методов от каких-либо объектов. Когда они вызываются браузером, переменная this указывает на глобальный объект window. Браузер также не передает этим функциям никакой информации о событиях таймера.

После нашей борьбы с событиями курсора, перехват событий таймеров уже не представляет сложности. При задании таймера скопируйте встроенную переменную this, которая указывает на объект FadingTooltip, содержащий переменные состояний, в локальную переменную с именем self в лексическом контексте вызовов функций setTimeout и setInterval. Задайте описания для анонимных функций, которые используют переменную self, и передайте эти функции в качестве аргумента функциям setTimeout и setInterval. Благодаря этому переменная self будет включена в описание функции, поэтому она останется доступной при вызове функций браузером, как показано в листинге 6.

Листинг 6. Код JavaScript для задания таймера и перехвата событий таймера

                
FadingTooltip.prototype = { 
    ...
    startTimer: function(timeout) { 
        var self = this;
        this.currentTimer = 
            setTimeout( function() { self.handleEvent( { type: 'timeout' } ); }, 
            timeout);
    },
    startTicker: function(interval) { 
        var self = this;
        this.currentTicker = 
            setInterval( function() { self.handleEvent( { type: 'timetick' } ); }, 
            interval);
    },
    ...

Получившиеся функции событий таймера делают не больше, чем функции для событий курсора. Они создают тривиальный объект события таймера, который содержит только один тип событий -- либо timeout , либо timetick -- и передает его тому же методу handleEvent, который выполняет обработку событий курсора.

Создание таблицы действий/переходов

В JavaScript прототипы объектов могут содержать такие структуры данных, как массивы и другие объекты, а также переменные и методы. Элементы обычных массивов индексируются по целым числам, тогда как элементы ассоциативных массивов индексируются не по числам, а по именам. В JavaScript ассоциативные массивы и объекты имеют совершенно разный синтаксис для доступа к одним и тем же данным: к свойствам объекта можно обращаться как к элементам ассоциативного массива, как показано в листинге 7.

Листинг 7. Код JavaScript для доступа к свойствам объекта как к элементам ассоциативного массива

                
if ( htmlElement.fadingTooltip == htmlElement["fadingTooltip"] ) ... // always true

Вы сможете воспользоваться преимуществами этого подхода, если реализуете свою таблицу состояний как двумерный ассоциативный массив функций. Используйте имена состояний и событий непосредственно в качестве индексов массива. Непустые ячейки массива укажут на анонимные функции, которые выполнят действия для событий посредством вызова вспомогательных методов (таких как запуск и отмена таймеров), а затем возвратят следующее состояние. Ядро метода handleEvent вызовет эти функции действия/перехода, используя синтаксис массива, примерно так, как показано в листинге 8.

Листинг 8. Код JavaScript для вызова анонимных функций, хранящихся в ассоциативном массиве

                
var nextState = this.actionTransitionFunctions[this.currentState][event.type](event);

Метод handleEvent обратится к таблице actionTransitionFunctions как к ассоциативному массиву, используя текущее состояние и тип события в качестве индексов, и выберет вызываемую функцию. Он передаст объект события этой функции в качестве аргумента. Функция выполнит необходимые действия, а затем возвратит имя следующего состояния.

Поскольку ассоциативные массивы являются объектами (и наоборот), можно определить таблицу actionTransitionFunctions, используя синтаксис объекта, даже если метод handleEvent обратится к ней, используя синтаксис массива. Например, в исходном состоянии Inactive единственным ожидаемым событием будет mouseover, поэтому можно задать описание для функции для обработки этой ситуации, как показано в листинге 9.

Листинг 9. Код JavaScript для сохранения анонимной функции в качестве свойства объекта

                
FadingTooltip.prototype = { 
    ...
    initialState: 'Inactive',
    actionTransitionFunctions: { 
        Inactive: {
            mouseover: function(event) { 
                this.cancelTimer();
                this.saveCursorPosition(event);
                this.startTimer(this.pauseTime*1000);
                return 'Pause';
            }
        },
        ...

Прототип для объекта FadingTooltip object включает свойство actionTransitionFunctions, значение которого - другой объект. Он включает свойство с именем Inactive, значение которого еще один объект. Он содержит одно свойство, с именем mouseover, значение которого - функция. Если событие mouseover наступит в состоянии Inactive, метод handleEvent вызовет эту функцию. Функция будет ожидать аргумента с именем event, выполнит три действия, вызвав три вспомогательных функции, а затем возвратит Pause в качестве имени следующего состояния. Как одно из действий, будет выполнено сохранение позиции курсора, которую браузеры хранят в объектах событий мыши, и запуск таймера, значение таймаута которого - это параметр с именем pauseTime (заданный в секундах, которые будут переведены в миллисекунды, как требует метод startTimer).

Нашему виджету придется реагировать на три различных события в состоянии Pause: mousemove, mouseout и timeout. Задайте определение для объекта Pause в таблице actionTransitionFunctions, которая содержит свойства для каждого из этих типов событий, как показано на рисунке 10.

Листинг 10. Код JavaScript для функции, которая реагирует на события курсора в состоянии Pause

                
FadingTooltip.prototype = { 
    ...
    actionTransitionFunctions: { 
        ...
        Pause: {
           mousemove: function(event) { 
              return this.doActionTransition('Inactive', 'mouseover', event);
           },
           mouseout: function(event) { 
              this.cancelTimer();
              return 'Inactive';
           },
           timeout: function(event) { 
              this.cancelTimer();
              this.createTooltip();
              this.startTicker(1000/this.fadeRate);
              return 'FadeIn';
           }
        },
        ...

Когда в состоянии Pause произойдет событие mousemove, метод handleEvent вызовет функцию, которая просто вызовет метод doActionTransition, передав свой аргумент event и возвратив результат, который возвратит этот метод. Метод doActionTransition, как вы могли догадаться, обращается к таблице actionTransitionFunctions, используя свои первые два аргумента как индексы массива, аналогично методу handleEvent , и передает свой третий аргумент функции, которую он обнаружит в этом массиве. Когда произойдет событие mouseout, наш код вызовет функцию, которая отменит таймер, запущенный ранее в данном разделе, а затем перейдет обратно в состояние Inactive.

Или, если произойдет событие timeout, вы отмените все таймеры, которые могут выполняться, создадите подсказку с исходной непрозрачностью, равной нулю, запустите тикер и выполните переход в состояние FadeIn.

В качестве еще одного примера функции из таблицы actionTransitionFunctions, задайте описание функции для обработки событий timetick в состоянии FadeIn как показано в листинге 11.

Листинг 11. Код JavaScript для функции, которая реагирует на события таймера в состоянии FadeIn

                
FadingTooltip.prototype = { 
    ...
    actionTransitionFunctions: { 
        ...
        FadeIn: {
            ...
            timetick: function(event) {
                this.fadeTooltip(+this.tooltipOpacity/(this.fadeinTime*this.fadeRate));
                if (this.currentOpacity>=this.tooltipOpacity) {
                    this.cancelTicker();
                    this.startTimer(this.displayTime*1000);
                    return 'Display';
                }
                return this.CurrentState;
            }
        },
        ....

При каждом наступлении события timetick в состоянии FadeIn метод handleEvent вызовет функцию, которая немного увеличит непрозрачность подсказки. Параметры функции: продолжительность постепенного усиления видимости (в секундах), параметры анимации нарастания непрозрачности, начиная от нуля (в шагах в секунду), и максимальная непрозрачность (заданная в виде плавающей величины между 0.0 и 1.0). Функция возвратит текущее состояние, оставив конечный автомат в состоянии FadeIn до тех пор, пока параметр непрозрачности подсказки не достигнет максимального значения. Затем она отменит тикер, запустит таймер на отображение подсказки и выполнит переход в состояние Display.

Оставшаяся часть функций в таблице actionTransitionFunctions описывается аналогичным способом. Подробности смотрите в полном исходном коде, который снабжен большим количеством комментариев; сравните этот исходный код с рисунком 1.

Реализация обработчика событий

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

Листинг 12. Код JavaScript для обработчика событий

                
FadingTooltip.prototype = { 
    ...
    handleEvent: function(event) { 
        var actionTransitionFunction = 
            this.actionTransitionFunctions[this.currentState][event.type];
        if (!actionTransitionFunction) 
            actionTransitionFunction = this.unexpectedEvent;
        var nextState = actionTransitionFunction.call(this, event);
        if (!this.actionTransitionFunctions[nextState]) 
            nextState = this.undefinedState(nextState);
        this.currentState = nextState;
    },
    ... 

В действительности реализация доступа к таблице actionTransitionFunctions отличается от кода, рекомендуемого в предыдущем разделе. Этот метод выбирает вызываемую функцию из таблицы actionTransitionFunctions , используя текущее состояние и тип события в качестве индексов ассоциативного массива. Однако метод копирует указатель в локальную переменную выбранной функции, а затем вызывает функцию при помощи метода call объекта функции, вместо того, чтобы вызвать ее напрямую. Это можно сделать потому, что объекты функций могут быть присвоены переменным, так же, как любое другое значение. Это нужно сделать потому, что встроенная переменная this должна указывать на объект FadingTooltip во время выполнения функции. Если бы вы действительно вызвали функцию напрямую из таблицы actionTransitionFunctions, используя индексы массива, как было описано ранее, то переменная this указала бы на таблицу. Метод call функции присваивает переменной this свой первый аргумент, а затем вызывает функцию, передавая остальную часть аргумента.

Не забывайте, что таблица actionTransitionFunctions является разреженной; мы ввели описания функций для событий, которые ожидаются в каждом состоянии, а все остальные ячейки оставили пустыми. Метод handleEvent для обработки любого непредвиденного события вызовет метод unexpectedEvent. Или, если функция действие/переход возвратит некоторое значение, которое не является корректным состоянием, она вызовет метод undefinedState. Эти методы отменят все запущенные таймеры, удалят все подсказки, если таковые были созданы, и возвратят конечный автомат в исходное состояние. Один из таких методов показан в листинге 13; другие почти идентичны ему.

Листинг 13. Код JavaScript для обработчика непредвиденного события

                
FadingTooltip.prototype = { 
    ...
    unexpectedEvent: function(event) { 
        this.cancelTimer();
        this.cancelTicker();
        this.deleteTooltip();
        alert('FadingTooltip received unexpected event ' + event.type + 
              ' in state ' + this.currentState);
        return this.initialState; 
    },	
    ... 

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

И наконец, отображение подсказки

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

Мы хотим, чтобы подсказка появлялась рядом с курсором, когда событие timeout происходит в состоянии Pause, но браузеры не передают информацию о позиции курсора событиям таймера. К счастью, браузеры передают информацию о позиции курсора событиям курсора, поэтому нужно сохранить эти данные в переменных состояния, вызвав метод saveCursorPosition при наступлении события курсора, как показано в листинге 14.

Листинг 14. Код JavaScript для сохранения позиции курсора

                
FadingTooltip.prototype = { 
    ...
    saveCursorPosition: function(event) {
        this.lastCursorX = event.clientX;
        this.lastCursorY = event.clientY;
    },
    ... 

Подсказка представляет собой HTML-элемент Division, содержащий некоторый текст, изображения и разметку, которые передаются в конструктор в аргументе tooltipContent. Метод createTooltip показан в листинге 15.

Листинг 15. Код JavaScript для создания подсказки

                
FadingTooltip.prototype = { 
    ...
    createTooltip: function() {     
        this.tooltipDivision = document.createElement('div');
        this.tooltipDivision.innerHTML = this.tooltipContent;
        
        if (this.tooltipClass) {
            this.tooltipDivision.className = this.tooltipClass;
        } else {
            this.tooltipDivision.style.minWidth = '25px';
            this.tooltipDivision.style.maxWidth = '350px';
            this.tooltipDivision.style.height = 'auto';
            this.tooltipDivision.style.border = 'thin solid black';
            this.tooltipDivision.style.padding = '5px';
            this.tooltipDivision.style.backgroundColor = 'yellow';
        }
        
        this.tooltipDivision.style.position = 'absolute';
        this.tooltipDivision.style.zIndex = 101;
        this.tooltipDivision.style.left = this.lastCursorX + this.tooltipOffsetX;
        this.tooltipDivision.style.top = this.lastCursorY + this.tooltipOffsetY;
        
        this.currentOpacity = this.tooltipDivision.style.opacity = 0;
        
        document.body.appendChild(this.tooltipDivision);                
    },	
    ... 

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

При каждом наступлении события timetick в состоянии FadeIn или FadeOut будет вызван метод fadeTooltip, который до некоторой степени увеличит или уменьшит непрозрачность подсказки, обеспечивая в то же время, чтобы она оставалась в диапазоне от нуля до максимального значения параметра, как показано в листинге 16.

Листинг 16. Код JavaScript для постепенного появления/пропадания подсказки

                
FadingTooltip.prototype = { 
    ...
    fadeTooltip: function(opacityDelta) { 
        this.currentOpacity += opacityDelta;
        if (this.currentOpacity<0) 
            this.currentOpacity = 0;
        if (this.currentOpacity>this.tooltipOpacity) 
            this.currentOpacity = this.tooltipOpacity;
        this.tooltipDivision.style.opacity = this.currentOpacity;
    },	
    ... 

Функции действие/переход также нуждаются во вспомогательных методах для перемещения и удаления подсказки. Реализация этих методов не представляет сложности и полностью описывается в примечаниях к исходному коду.

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

Листинг 17. Код JavaScript для задания параметров прототипа объекта

                
FadingTooltip.prototype = { 
    ...
    tooltipClass: null,  // имя стиля CSS , который следует применить к подсказке
                         // 'null' для стиля по умолчанию
    tooltipOpacity: 0.8, // максимальная непрозрачность подсказки, 
                         // от 0.0 до 1.0 включительно 
                         // (после усиления, до начала ослабления)
    tooltipOffsetX: 10,  // горизонтальное смещение курсора от левого верхнего 
                         // угла подсказки
    tooltipOffsetY: 10,  // вертикальное смещение курсора от левого верхнего
                         // угла подсказки
    fadeRate: 24,        // частота фаз анимации для усиления и ослабления 
                         // шагов в секунду
    pauseTime: 0.5,      // на какое время курсор должен задержаться над HTML-элементом,
                         // чтобы началось появление (усиление непрозрачности) 
                         // подсказки, в секундах
    displayTime: 10,     // сколько времени отображать подсказку (после появления
                         // до начала исчезновения (ослабления непрозрачности),в секундах
    fadeinTime: 1,       // время анимации появления подсказки, в секундах
    fadeoutTime: 3,      // время анимации исчезновения подсказки, в секундах
    ... 
};

Дополнительный аргумент parameters конструктора объекта представляет собой объект, закодированный в нотации JavaScript Object Notation (которую иногда называют JSON), который может отменить значения по умолчанию для любого из этих свойств, как показано в листинге 18.

Листинг 18. Код JavaScript для инициализации параметров в конструкторе объекта

                
function FadingTooltip(htmlElement, tooltipContent, parameters) { 
    ...
    for (parameter in parameters) { 
        if (typeof(this[parameter])!='undefined') 
            this[parameter] = parameters[parameter];
    }
    ...
};

Конструктор проверяет каждое свойство в своем аргументе parameters; для каждого из них, если свойство существует в прототипе, его значение отменяет значение параметра по умолчанию. Не забывайте, что прототип - это объект; кроме того, он также является ассоциативным массивом. Здесь мы опять используем нотацию объекта, чтобы определить параметры, и нотацию массива, чтобы обратиться к нему.

На этом реализация виджета FadingTooltip закончена. Вы можете загрузить реальный исходный код для конструктора и прототипа.

Несколько слов о производительности

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

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

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

Например, если значение параметра анимации по умолчанию равно 24 шагам в секунду, то у обработчика события timetick есть самое большее 40 миллисекунд на то, чтобы успеть сделать все, что необходимо, до возврата в браузер, при условии, что у него в распоряжении все ресурсы процессора. На современных рабочих станциях за это время можно сделать достаточно много. Тем не менее, ваша задача заключается не в том, чтобы сделать за доступный период времени как можно больше, а в том, чтобы использовать как можно меньше процессорного времени. Если использование процессора вашей реализацией программы незначительно, то анимация будет выполняться плавно, а наша программа будет реагировать на запросы даже в тех случаях, когда процессор будет перегружен другими задачами.

Не поддавайтесь искушению увеличить частоту шагов анимации до 60 или 85 в секунду, думая, что если она будет соответствовать частоте обновления вашего монитора, то анимация получится более плавной. При этом временной промежуток между событиями timetick уменьшится приблизительно до 12 миллисекунд. Если ваш обработчик события timetick будет продолжаться дольше этого времени, или произойдет конфликт использования ресурсов процессора, то анимация может демонстрироваться скачками, а браузер перестанет отвечать на запросы.

Готовность к тестированию

Закончив реализацию программы, вы подготовились к тому, чтобы протестировать ее на некоторых реальных браузерах. Об этом пойдет речь в третьей части данной серии статей. Однако не забывайте, что разработка - это итеративный процесс; возможно, придется вернуться к фазе проектирования или реализации...


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=7746