Pull to refresh

Разбираемся с анимацией в jQuery

jQuery *
imageЗдравствуйте. Сегодняшний топик я хотел написать о том, что механизм анимации в jQuery не эффективен, создает кучу таймеров, каждый из которых работает по отдельности, что приводит к чрезмерно частой перерисовки контента и сильно тормозит браузер, и хотел описать некоторые приемы написания «правильной анимации». В ходе подготовки примеров я понял, что я ошибался. Механизм анимации jQuery действительно не эффективен, создает кучу проблем, но причины этих проблем вовсе не в создании большого количества таймеров, а в нечте совсем другом, и, кажется, я добился замечательных результатов в устранении этих проблем.

Итак, речь пойдет о рассинхронизации движения анимированных объектов, даже если они все были анимированы одним вызовом animate.
Давайте рассмотрим небольшой пример одновременной анимации большого количества объектов:
<div class="container">
  <div class="animate">
  </div>
</div>
<script type="text/javascript">
  $(function(){
    for (var i = 0; i < 8; i++) {
      $('.animate').clone().appendTo('.container');
    }
    $('.animate').animate({
      left: 800
    }, 500);
  });
</script>

* This source code was highlighted with Source Code Highlighter.

В теории все эти объекты должны двигаться синхронно. На практике мы получаем такую картину:
image

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

Тем не менее причина рассинхронизации близка к множественности таймеров, она заключается в том, что для каждого элемента время старта его анимации считается независимо. Если элементов много, между моментами подсчета времени для первого и последнего элемента, может пройти достаточно много времени. Причем это время будет тем больше, чем медленнее javascript-движок браузера, что мы и видим на скриншоте — значение рассинхронизации у оперы 110 пикселей (скриншот в масштабе 1:2), у хрома 74 пикселей, а у другого, весьма популярного, браузера рассинхронизация больше ширины тестового забега, восьмиста пикселей.

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

А теперь самое главное. Учитывая то, что таймер один, подправить время запуска анимации не представляет сложности и за пару минут были внесены необходимые исправления, решающие эти проблемы. Исправления эти заключаются в том, что время старта анимации для всех элементов считается один раз при входе в функцию animate. Я даже пошел дальше, внес в опции функции animate дополнительный параметр startTime, который может принимать пользовательское значение времени начала анимации. Это позволит синхронизировать любое количество вызовов функции animate между собой.

Помимо синхронизации, можно использовать начальное значение времени еще для нескольких интересных штук:
1) Задать время старта анимации в прошлом и начинать анимацию с середины. Удобно тем, что не придется делать отдельную функцию изинга (easing) для половинки анимации.
2) Задать время старта в будущем, дав медленным движкам браузеров (привет, IE!) прожевать все объекты. Тут, правда, нужна функция изинга, приводящая отрицательные смещения времени к нулевой координате. стандартные linear и swing не обладают такими свойствами.
3) Делать анимацию в несколько проходов или бесконечную анимацию, задавая время старта анимации в будущем. Для этого нужна будет функция изинга, способная верно обработать отрицательное смещение времени. В силу того, что стандартная функция изинга swing построена на Math.cos, она удовлетворяет этому требованию.

Примечание, для тех кто не в курсе — функции изинга (easing, по русски, наверное это смягчение), это функции, принимающие значения от 0 до 1, где ноль — начальное время, 1 — конечное время анимации, и возвращающие значение также от 0 до 1, где 0 — начальное значение изменяемого свойства, а 1 — конечное. В общем случае значения свойств могут лежать за пределами 0 и 1, ну и с моим патчем время тоже может быть меньше нуля.

Изменения, которые я внес в код

Время текущего шага вычислялось в функции jQuery.fx.step так:
var t = now();
Я добавил функции еще один параметр time и изменил определение времени так:
var t = time || now();

В функцию jQuery.fx.custom пришлось добавить еще один параметр startTime и вычислять время старта анимации похожим образом с предыдущей функцией:
this.startTime = startTime || now();

Кроме того, внутри того самого единственного setInterval, было добавлено получение времени:
var time = now();
и последующая передача этого времени в функции из массива, jQuery.timers[], который содержит функции jQuery.fx.step.

В функции animate объект optall был дополнен параметром startTime:
optall = jQuery.extend({startTime: now()}, optall)

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

Исправленная версия jQuery

upd. Собственно основную засаду такого способа я нашел, она проявляется при использовании очередей. Впрочем все не слишком безнадежно, мне кажется я смогу и это обойти. Как только я буду уверен в безглючности своего решении (или в невозможности его реализовать :) ), свяжусь с автором jQuery.

PS А еще я вчера столкнулся с нерабочей прозрачностью в Опере 9.2 и последних версиях jQuery. Описание проблемы и временное решение.
Tags: jQueryanimate
Hubs: jQuery
Total votes 166: ↑160 and ↓6 +154
Comments 35
Comments Comments 35

Popular right now