Создаем прокрутку списка записей по типу Твиттера без jQuery

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

 

Введение.

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

Целью нашего материала является показать, что на jQuery можно смотреть как на обычный JavaScript, в котором нет ничего магического. Иногда, ваш код может обладать большей "магией, если из него исключить немного великолепия jQuery.  Очень может быть, что после усвоения данного урока вы будете понимать JavaScript немного лучше.

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

 

Шаг 1. Проект

Вот что мы будем строить. Твиттер для Mac показал отличную идею для реализации. Если запустить приложение и перейти на чью-нибудь страницу, то при прокрутке вниз  станет видно, что иконка аватара присутствует не у всех записей, а "следует" за прокруткой страницы. Если встречаются следующие подряд несколько записей, то аватара выводится только для одной, расположенной на самом верху. При изменении автора записи изменяется и аватар.

Именно такой функционал мы и реализуем. С использованием jQuery выполнить задачу будет достаточно просто.

 

Шаг 2. HTML и CSS

Прежде чем начать работать над кодом, нужно построить разметку. Для нашего урока она будет выглядеть следующим образом:

<!DOCTYPE HTML>
<html lang="ru">
    <head>
        <meta charset="UTF-8">
        <title>Прокрутка списка в стиле Twitter | Демонстрация для сайта RUSELLER.COM</title>
        <link rel="stylesheet" href="/style.css" />
    </head>
    <body>
        <section>
            <article>
            <img class="avatar" src="/images/one.jpg" />
            <p> Запись, сделанная пользователем.</p>
            </article>
            <article>
            <img class="avatar" src="/images/two.jpg" />
            <p> Запись, сделанная пользователем.</p>
            </article>
            <article>
            <img class="avatar" src="/images/one.jpg" />
            <p>  Запись, сделанная пользователем. 
            Просто текст для демонстрации.</p>
            </article>
            <article>
            <img class="avatar" src="/images/one.jpg" />
            <p> Запись, сделанная пользователем.</p>
            </article>

            <!-- Здесь добавляем еще записей!-->

        </section>

        <script src="/jquery.js"></script>
        <script src="/twitter-scrolling.js"></script>

        <script>
            $("section").twitter_scroll(".avatar");
        </script>
    </body>
</html>

Да, код получился длинным.

Теперь добавим CSS.

body {
    font:13px/1.5 "helvetica neue", helvetica, arial, san-serif;
}
article {
    display:block;
    background: #ececec;
    width:380px;
    padding:10px;
    margin:10px;
    overflow:hidden;
}
article img {
    float:left;
}
article p {
    margin:0;
    padding-left:60px;
}

Здесь только то, что действительно нужно для нашего урока.

 

Шаг 3. JavaScript. Раунд 1.

Надо сказать, что задача выходит за рамки создания виджета JavaScript средней сложности. Вот несколько моментов, которые надо принять во внимание при разработке:

  • Нужно скрывать каждое изображение, если оно одинаковое с предыдущей записью.
  • При прокрутке страницы надо определять, какая запись находится наиболее близко к верху страницы.
  • Если запись является первой в серии, сделанной одной персоной, то необходимо зафиксировать аватар, чтобы он не прокручивался с остальным содержанием страницы.
  • Когда запись является последней в серии одного автора, нужно оставить аватар на соответствующей строке.
  • Все выше описанные моменты должны работать при прокрутке и вверх и вниз.
  • Так как все выполняется каждый раз, при генерации события scroll, то код должен работать быстро.

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

Будем писать код в виде плагина jQuery:

$(wrapper_element).twitter_scroll(entries_element, unscrollable_element);  

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

$("section").twitter_scroll("article", ".avatar");

// или

$("section").twitter_scroll(".avatar");

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

Оболочка плагина:

(function ($) {

    jQuery.fn.twitter_scroll = function (entries, unscrollable) { 

    };
}(jQuery));

 

Установки плагина

Начнем с кода установок. Нужно сделать несколько действий, прежде чем определять обработчик события scroll.

if (!unscrollable) {
    unscrollable = $(entries);
    entries = unscrollable.parent();
} else {
    unscrollable = $(unscrollable);
    entries = $(entries);
}

Первый параметр: если unscrollable имеет значение false, то мы с помощью jQuery и селектора entries мы устанавливаем значение unscrollable , а самой переменной  entries присваиваем значение родителя unscrollable. Иначе, с помощью jQuery устанавливаются оба параметра. Теперь (если пользователь правильно установил разметку) у нас есть два объекта jQuery, которые соответствуют друг другу и имеют одинаковый индекс: unscrollable[i] является потомком entries[i]. Это пригодиться в дальнейшем. Если предполагать, что пользователь разметил свой документ не правильно, или что используются селекторы, которые выбирают элементы вне нужных нам областей, то можно использовать  this в качестве параметра контекста или метод find для this.

Далее, определим несколько переменных:

var parent_from_top  = this.offset().top,
    entries_from_top = entries.offset().top,
    img_offset       = unscrollable.offset(),
    prev_item        = null,
    starter          = false,
    margin           = 2,
    anti_repeater    = null,
    win              = $(window),
    scroll_top       = win.scrollTop();

Смещения элементов, с которыми мы работаем, очень важны. Метод jQuery offset возвращает объект со свойствами top и left. Для родительского элемента (this внутри плагина) и для самого элемента entries нам нужны только смещения сверху. Для непрокручиваемых  элементов нам нужно использовать смещения сверху и слева.

Переменные prev_item, starter, и anti_repeater будут использоваться далее в обработчика события scroll, где нужны будут значения, существующие независимо от вызовов функций. Переменная win будет использована в нескольких местах. А переменная scroll_top содержит расстояние панели прокрутки от верха экрана. Мы будем использовать ее для определения направления прокрутки.

Далее мы определим, какие элементы являются первым и последним в полосе записей. Существует несколько способов выполнить задачу. Мы будем использовать атрибут HTML5 data для соответствующих элементов.

entries.each(function (i) {
    var img = unscrollable[i];
    if ($.contains(this, img)) {
        if (img.src === prev_item) {
            img.style.visibility = "hidden";
            if (starter) {
                entries[i-1].setAttribute("data-8088-starter", "true");
                starter = false;
            }
            if (!entries[i+1]) {
                entries[i].setAttribute("data-8088-ender", "true");
            }
        } else {
            prev_item = img.src;
            starter = true;
            if (entries[i-1] && unscrollable[i-1].style.visibility === "hidden") {
                entries[i-1].setAttribute("data-8088-ender", "true");
            }
        }
    }
});

prev_item = null;

Мы используем метод jQuery each для объекта entries. Помните, что внутри функции this указывает на текущий элемент из entries. Также у нас есть параметр индекс, который мы используем. Мы получаем соответствующий элемент из объекта unscrollable и сохраняем его в img. Затем, если  наш элемент содержит img (так должно быть, но нам нужно проверить), мы проверяем, что источник изображения тот же, что и prev_item. Если это верно, то мы знаем, что изображение такое же, как и у предыдущей записи. Следовательно, надо скрыть изображение. Использовать свойство display нельзя, так как такое действие будет вызывать удаление изображения из потока документа, а нам не нужно перемещение других элементов на пустое место.

Затем, если значение startertrue, мы устанавливаем предыдущему элементу атрибут data-8088-starter. Все атрибуты HTML5 data должны начинаться с “data-“. А использование собственного суффикса позволяет избежать конфликта с кодом других разработчиков (в данном случае используется код 8088). Атрибут HTML5 data должен иметь строковое значение, но в нашем случае мы используем его только как маркер элемента. Затем устанавливаем значение starterfalse. И если это последний элемент, то маркируем соответствующим образом.

Если источник изображения не совпадает с предыдущим элементом, то изменяем значение prev_item.  Затем устанавливаем значение startertrue. Таким образом, если следующее изображение будет иметь такой же источник, мы промаркируем данный элемент как стартовый. Далее, если есть элемент перед текущим, и его изображение скрыто, то мы знаем, что он является последним в серии с одинаковым изображением (потому что текущий элемент имеет другое изображение). Следовательно, нужно установить соответствующий атрибут маркер.

По завершению обработки установим значение prev_item  null, так как вскоре будем использовать его снова.

Теперь, если взглянуть на разметку после работы функции (например, с помощью Firebug), можно увидеть следующую структуру:

 

Обработчик события scroll

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

$(document).bind("scroll", function (e) {
    var temp_scroll = win.scrollTop(),
        down = ((scroll_top - temp_scroll) < 0) ? true : false,
        top_item = null, child = null;

    scroll_top = temp_scroll;

    // Здесь будет вставляться код

});

Это оболочка нашего обработчика. Создаем несколько переменных. Переменная down определяет направление прокрутки. Переменная scroll_top содержит расстояние от верха, на которое произошла прокрутка.

Теперь присвоим переменной  top_item самый близкий к верху страницы элемент:

top_item = entries.filter(function (i) {
    var distance = $(this).offset().top - scroll_top;
    return ( distance < (parent_from_top + margin) && distance > (parent_from_top - margin) );
});

Мы просто используем метод filter для определения элемента, который должен попасть в перменную top_item. Сначала определяем значение distance как разность уже прокрученного расстояния и смещения элемента сверху. Затем, возвращаем true, если значение distance находится между parent_from_top + margin и parent_from_top - margin. Нам необходимо учесть смещение контейнера сверху. Кроме того, событие scroll может генерироваться при некотором смещении от точного значения положения верха. Поэтому надо учесть поле, чтобы использовать диапазон, а не определённую точку.

Теперь у нас есть верхний элемент и можно работать дальше.

if (top_item) {
    if (top_item.attr("data-8088-starter")) {
        if (!anti_repeater) {
            child = top_item.children(unscrollable.selector);
            anti_repeater = child.clone().appendTo(document.body);
            child.css("visibility", "hidden");
            anti_repeater.css({
                'position' : 'fixed',
                'top'      : img_offset.top + 'px',
                'left'     : img_offset.left + "px"
            });
        }
    } else if (top_item.attr("data-8088-ender")) {
        top_item.children(unscrollable.selector).css("visibility", "visible");
        if (anti_repeater) { anti_repeater.remove(); }
        anti_repeater = null;
    } 

    if (!down) {
        if (top_item.attr("data-8088-starter")) {
            top_item.children(unscrollable.selector).css("visibility", "visible");
            if (anti_repeater) { anti_repeater.remove(); }
            anti_repeater = null;
        } else if (top_item.attr("data-8088-ender")) {
            child = top_item.children(unscrollable.selector);
            anti_repeater = child.clone().appendTo(document.body);
            child.css("visibility", "hidden");
            anti_repeater.css({
                'position' : 'fixed',
                'top'      : img_offset.top + 'px',
                'left'     : img_offset.left + "px"
            });
        }
    }
}

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

Если верхний элемент имеет атрибут data-8088-starter , то поверяем значение переменной anti_repeater. Данная переменная указывает на элемент изображения, который фиксируется при прокрутке. Если значение anti_repeater еще не установлено, то получаем наследника нашего элемента top_item, который имеет такой же селектор, как и unscrollable (далее код будет оптимизирован). Затем мы клонируем его и добавляем к body.  Затем скрываем оригинал, а клон размещаем туда, куда нужно.

Если элемент не имеет атрибута data-8088-starter, то проверяем наличие атрибута data-8088-ender. Если данный атрибут присутствует, находим потомка и делаем его видимым, а затем устанавливаем значение переменной anti_repeater  null.

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

 

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

Итак, наш код делает то, что нужно. Но при проверке видно, что прокрутка осуществляется очень медленно. Можно добавить строки console.profile("scroll") и console.profileEnd() к первой и последней строкам функции обработки события scroll. Обработка может занять от 2.5ms до  4ms, и будет сделано 166 – 170 вызовов функции.

Очень медленная обработка. Некоторые функции вызываются 30-31 раз. В нашем тестовом списке 30 элементов. Поэтому, чем больше элементов мы будем иметь в списке, тем медленнее будет работать прокрутка! Нужно искать пути оптимизации процесса.

 

Шаг 4. JavaScript. Раунд 2

Если вы подозреваете, что jQuery является главным виновником данного безобразия, то окажетесь правы. Библиотеки, подобные jQuery очень удобны и облегчают работу с DOM, но плата за удобство - производительность. В нашем случае нужно использовать более продуктивный код. Откажемся от части кода jQuery в пользу непосредственной работы со структурой DOM, что, на самом деле, окажется не таким уж сложным процессом.

 

Обработчик события Scroll

Разберемся с очевидной частью, с тем, что мы делаем с top_item, как только находим его. В текущий момент top_item является объектом jQuery. Однако, все что мы делаем с помощью jQuery с top_item тривиально сделать без использования библиотеки.

Вот что можно изменить для ускорения процесса:

  • Перестроить выражение if для исключения повторений (это, скорее, касается структуры кода, а не производительности).
  • Можно использовать метод JavaScript  getAttribute вместо метода jQuery attr.
  • Можно получать элемент из unscrollable, который соответствует элементу top_item, вместо использования unscrollable.selector.
  • Можно использовать методы JavaScript clodeNode и appendChild вместо версий jQuery.
  • Можно использовать свойство style вместо метода jQuery css.
  • можно использовать метод JavaScript removeNode вместо метода jQuery remove.

Применив все идеи, получим следующий код:

if (top_item) {
    if ( (down && top_item.getAttribute("data-8088-starter")) || ( !down && top_item.getAttribute("data-8088-ender") ) ) {
        if (!anti_repeater) {
            child = unscrollable[ entries.indexOf(top_item) ];
            anti_repeater = child.cloneNode(false);
            document.body.appendChild(anti_repeater);
            child.style.visibility = "hidden";
            style = anti_repeater.style;
            style.position = 'fixed';
            style.top = img_offset.top + 'px';
            style.left= img_offset.left + 'px';
        }
    }
    if ( (down && top_item.getAttribute("data-8088-ender")) || (!down && top_item.getAttribute("data-8088-starter")) ) {
        unscrollable[ entries.indexOf(top_item) ].style.visibility = "visible";
        if (anti_repeater) { anti_repeater.parentNode.removeChild(anti_repeater); }
        anti_repeater = null;
    }
}


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

Также оптимизации можно подвергнуть процесс получения  top_item. В текущий момент мы используем метод jQuery filter. Если вы хорошо подумаете, то распознаете ущербность такого способа. Мы знаем, что будем получать только один элемент с помощью фильтра, но метод filter об этом не знает и продолжает работать после того, как нужный элемент найден. В тестовом списке 30 элементов, так что перебор занимает достаточное количество времени. Сделаем вот так:

for (i = 0; entries[i]; i++) {
    distance = $(entries[i]).offset().top - scroll_top;
    if ( distance < (parent_from_top + margin) && distance > (parent_from_top - margin) ) {
        top_item = entries[i];
        break;
    }
}

(В качестве альтернативы можно использовать цикл while с условием !top_item. Разницы, практически, никакой нет.)

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

Как реализовать задуманные изменения? Начнем с сохранения top_item от предыдущего запуска обработчика события scroll:

Присвоение надо поместить в самый конец функции обработчика. Теперь можно использовать имеющееся значение:

if (prev_item) {
    prev_item = $(prev_item);
    height = height || prev_item.outerHeight();
    if ( down && prev_item.offset().top - scroll_top + height < margin) {
        top_item = entries[ entries.indexOf(prev_item[0]) + 1];
    } else if ( !down && (prev_item.offset().top - scroll_top - height) > (margin + parent_from_top)) {
        top_item = entries[ entries.indexOf(prev_item[0]) - 1];
    } else {
        top_item = prev_item[0];
    }
} else {
    for (i = 0; entries[i]; i++) {
        distance = $(entries[i]).offset().top - scroll_top;
        if ( distance < (parent_from_top + margin) && distance > (parent_from_top - margin) ) {
            top_item = entries[i];
            break;
        }
    }
}

Если переменная prev_item имеет значение, значит, мы можем использовать ее для поиска следующего элемента top_item. Здесь используется небольшой трюк:

  • Если предыдущий элемент находится полностью над верхом страницы, берем следующий элемент  (можно использовать индекс prev_item + 1 для получения нужного элемента в entries).
  • Если прокрутка происходит вверх, и предыдущий элемент достаточно далеко под страницей, берем предшествующий ему элемент.
  • В противном случае используем тот же элемент, что и в последний раз.
  • Если нет значения в top_item, то используем цикл для поиска.

Есть еще несколько моментов. Что такое переменная height? Если все элементы имеют одинаковую высоту, то мы можем исключить вычисление высоты при каждом вызове обработчика события scroll, перенеся вычисления в установки плагина. Добавляем в раздел установок:

height = entries.outerHeight();
if (entries.length !== entries.filter(function () { return $(this).outerHeight() === height; }).length) {
    height = null;
}

Метод jQuery outerHeight возвращает высоту элемента, включая отступы и поля. Если все элементы имеют одинаковую высоту с первым элементом, то height будет установлена, а в противном случае значение height будет null. При определении top_item можно использовать height, если она имеет значение.

Есть еще один момент. Можно подумать, что вычисление  prev_item.offset().top выполняется дважды. В действительности оно выполняется только один раз, так как второе выражение if  выполняется только в случае, если значение down является false. Так как мы используем логический оператор AND, то вторая часть двух выражений if никогда не будет вызываться в одно и то же время. Конечно же, вы можете использовать перменную, если считаете нужным в данном случае, но она никак не повлияет на производительность.

Однако есть еще над чем поработать. Безусловно, наш обработчик стал быстрее, но мы можем сделать дополнительные улучшения. Мы используем jQuery только для двух процессов: получаем outerHeight для prev_item, и получаем смещение от верха для prev_item. Для таких мелочей слишком большое расточительство, использовать объекты jQuery. Но моментального решения для замещения jQuery в данном случае нет. Нужно погрузиться в код jQuery, чтобы точно определить, что делает библиотека.  Таким образом, мы сможем заменить небольшую часть кода без использования избыточного веса остального jQuery.

Начнем со смещения от верха. Вот код jQuery для метода offset:

function (options) {
    var elem = this[0];

    if (!elem || !elem.ownerDocument) {
        return null;
    }

    if (options) {
        return this.each(function (i) {
            jQuery.offset.setOffset(this, options, i);
        });
    }

    if (elem === elem.ownerDocument.body) {
        return jQuery.offset.bodyOffset(elem);
    }

    var box = elem.getBoundingClientRect(),
        doc = elem.ownerDocument,
        body = doc.body,
        docElem = doc.documentElement,
        clientTop = docElem.clientTop || body.clientTop || 0,
        clientLeft = docElem.clientLeft || body.clientLeft || 0,
        top = box.top + (self.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop) - clientTop,
        left = box.left + (self.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft) - clientLeft;

    return {
        top: top,
        left: left
    };
}

Выглядит сурово. Однако не все так плохо. Нам нужно только смещение сверху. Поэтому можно смело выкинуть часть кода, которая касается смещения слева и сделать свою функцию:

function get_top_offset(elem) {
    var doc = elem.ownerDocument,
        body = doc.body,
        docElem = doc.documentElement;

    return elem.getBoundingClientRect().top +
        (self.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop) -
            docElem.clientTop || body.clientTop || 0;
}


Все, что нужно сделать - это передать в нее элемент DOM. И мы получим смещение! Теперь можно заменить все вызовы offset().top нашей функцией.

А как на счет outerHeight? Здесь будет небольшой трюк, так как в данном случае используется внутренняя функция jQuery.

function (margin) {
    return this[0] ? jQuery.css(this[0], type, false, margin ? "margin" : "border") : null;
}

В действительности здесь происходит только вызов функции css с параметрами: элемент, “margin”, “border”, и false. Вот как она выглядит:

function (elem, name, force, extra) {
    if (name === "width" || name === "height") {
        var val, props = cssShow,
            which = name === "width" ? cssWidth : cssHeight;

        function getWH() {
            val = name === "width" ? elem.offsetWidth : elem.offsetHeight;

            if (extra === "border") {
                return;
            }

            jQuery.each(which, function () {
                if (!extra) {
                    val -= parseFloat(jQuery.curCSS(elem, "padding" + this, true)) || 0;
                }

                if (extra === "margin") {
                    val += parseFloat(jQuery.curCSS(elem, "margin" + this, true)) || 0;
                } else {
                    val -= parseFloat(jQuery.curCSS(elem, "border" + this + "Width", true)) || 0;
                }
            });
        }

        if (elem.offsetWidth !== 0) {
            getWH();
        } else {
            jQuery.swap(elem, props, getWH);
        }

        return Math.max(0, Math.round(val));
    }

    return jQuery.curCSS(elem, name, force);
}

В данной функции можно легко потеряться, но распутать логику стоит... Потому что решение удивительно! Все, что нам нужно - использовать свойство элемента offsetHeight!

Таким образом, наш код превращается в:

if (prev_item) {
    height = height || prev_item.offsetHeight;
    if ( down && get_top_offset(prev_item) - scroll_top + height < margin) {
        top_item = entries[ entries.indexOf(prev_item) + 1];
    } else if ( !down && (get_top_offset(prev_item) - scroll_top - height) > (margin + parent_from_top)) {
        top_item = entries[ entries.indexOf(prev_item) - 1];
    } else {
        top_item = prev_item;
    }
} else {
    for (i = 0; entries[i]; i++) {
        distance = get_top_offset(entries[i]) - scroll_top;
        if ( distance < (parent_from_top + margin) && distance > (parent_from_top - margin) ) {
            top_item = entries[i];
            break;
        }
    }
}

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

 

Полезный инструмент

В таких случаях очень полезным инструментом является проект jQuery Source Viewer, с помощью которого можно исследовать внутренности jQuery.

 

Заключение

В данном уроке мы рассмотрели процесс создания прокрутки списка записей. Но главное заключается в другом:

  • Факт, что ваш код выполняет то, что задумывалось, не означает, что он ясный, или быстрый, или его нельзя улучшить
  • jQuery — или другая ваша любимая библиотека — это простой JavaScript. Она тоже может быть медленной и иногда от нее лучше отказаться.
  • Лучший способ для развития разработчика - выйти из зоны комфорта.

 

Источник: http://feedproxy.google.com/~r/ruseller/CdHX/~3/F64BkzIs_-s/lessons.php

Читать комменты и комментировать

Добавить комментарий / отзыв



Защитный код
Обновить

Создаем прокрутку списка записей по типу Твиттера без jQuery | | 2012-06-19 12:08:29 | | Статьи Web-мастеру | | jQuery - удивительный инструмент. Но когда его не стоит использовать? В данном уроке мы рассмотрим  вопрос, как создать интересную прокрутку содержания, а затем пересмотрим наш проект на предмет | РэдЛайн, создание сайта, заказать сайт, разработка сайтов, реклама в Интернете, продвижение, маркетинговые исследования, дизайн студия, веб дизайн, раскрутка сайта, создать сайт компании, сделать сайт, создание сайтов, изготовление сайта, обслуживание сайтов, изготовление сайтов, заказать интернет сайт, создать сайт, изготовить сайт, разработка сайта, web студия, создание веб сайта, поддержка сайта, сайт на заказ, сопровождение сайта, дизайн сайта, сайт под ключ, заказ сайта, реклама сайта, хостинг, регистрация доменов, хабаровск, краснодар, москва, комсомольск |
 
Поделиться с друзьями: