Библиотека JQuery была создана в 2006 году для восполнения недостающей функциональности Javascript. С тех пор последний достаточно продвинулся в своем развитии, чтобы разработчики могли обходиться без JQuery, основная проблема которой — производительность.



На Хабре было несколько статей с объективными замерами «тормознутости» JQuery на селекторных запросах.
// jQuery 2.0
var c = $("#comments .comment");
4,649 ms
// jQuery 2.0 
var c = $(".comment");
3,437 ms
// native querySelectorAll
var c = document.querySelectorAll("#comments .comment");
1,362 ms
// native querySelectorAll
var c = document.querySelectorAll(".comment");
1,168 ms
// native getElementById / getElementsByClassName
var n = document.getElementById("comments"); 
var c = n.getElementsByClassName("comment");
107 ms
// native getElementsByClassName
var c = document.getElementsByClassName("comment");
75 ms
(запуск в цикле по 10000 раз)

В сети достаточно много хороших описаний аналогов JQuery функций на чистом Javascript-e — например, вот здесь.

Но самая мощь JQuery — в лаконичности и красоте ее выражений. Даже психологически тяжело переписывать существующий код, меняя элегантные $() на многострочные конструкции.

Попробуем, насколько возможно, оставить язык JQuery, [частично] заменив ее саму. Для этого нам нужно только или переопределить $() функцию, или заменить (что лучше) на свою — пусть это будет $jqr(). Она также будет возвращать объект, но уже «нативный» и не обремененный ненужными нам JQuery функциями.

Пример кода:

<html>
    <body>
        <p id="message"></p>
    </body>
</html>

JQuery код:

$("#message").html("Hello world!");

Меняется на:


$jqr("#message").html("Hello world!");


// JQuery Replacement
function $jqr(sel) {
    return new JQR(sel);
}

class JQR {
    constructor(sel) {
        this.sel = sel;
        this.elements = document.querySelectorAll(sel);
    }

    html(str) {
        for (var i = 0; i < this.elements.length; i++) {
            this.elements[i].innerHTML = str;
        }
    }
}

В конструкторе класса желательно парсить sel, чтобы более эффективно применять querySelectorAll(), getElementsByClassName() и getElementById().

Таким образом мы можем реализовывать в классе JQR только нужные нам функции, не выходя за рамки стандартного Javascript-а и не сильно трогая существующий код.

Необязательно даже полностью избавляться от JQuery — частичная оптимизация уже даст результат.

Комментарии (73)


  1. LazyProger
    07.08.2018 07:47
    -1

    Наблюдается следующее: JS как ЯП очень популярный и востребованный имея море фреймворков и библиотек эволюционирует в кита с солянкой. Jquery появился потому что не было querySelectorAll. Ждем когда он в себя заберет текущие фреймворки?


    1. altrus Автор
      07.08.2018 07:54

      Фреймворк — это не библиотека, фреймворк — это не язык.
      Кроме JQuery распространенных таких библиотек нет.


  1. dopusteam
    07.08.2018 07:52

    Что есть 'чистый javaScript'?


    jQuery — это не только методы для выборки


    1. altrus Автор
      07.08.2018 07:56

      Речь идет не о переписке JQuery, а о миграции проекта. У меня в последнем, например, используется методов 5-10 из JQuery, все они могут быть так переписаны.

      Речь тут только о селекторных функциях. AJAX и другое — это отдельная история.


      1. dopusteam
        07.08.2018 09:24
        +1

        Ну так это сложно назвать 'миграцией с jQuery', это 'миграция с 10 функций jQuery'.
        Не сочтите за занудство, но это всё таки абсолютно разные вещи.

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


        1. altrus Автор
          07.08.2018 09:35

          Это миграция сайта с JQuery на Javascript ES6. Сколько JQuery функций используется на сайте не играет роли.
          Миграция в данном случае это метод и процесс, а не конкретная библиотека-заменитель.


          1. dopusteam
            07.08.2018 09:45

            Ну так у вас ни метод ни процесс не описан.
            Нужно переписать функции jQuery — ну да, это логично.
            А что по поводу подводных камней, сложностей, каких то советов?

            Вашу статью можно уместить в одно предложение 'Напишите функции, подобные существующим в jQuery и используйте их, вместо jQuery' — ок

            С таким подходом, вы можете заменить jQuery в статье на что угодно (angular, knockout), раз вам не важно количество функции, тем более функция выборки везде есть, вот её и будем переписывать.


            1. altrus Автор
              07.08.2018 09:50

              Вашу статью можно уместить в одно предложение 'Напишите функции, подобные существующим в jQuery и используйте их, вместо jQuery' — ок

              Совершенно верно. Именно в этом смысл статьи.

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


              1. dopusteam
                07.08.2018 09:56

                Совершенно верно. Именно в этом смысл статьи.

                Был ли смысл целую статью писать?

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

                До тех по пока Вы не пишете сложных функций. Не для всех функции есть аналоги в чистом js.

                Если вы можете добиться такого повышения эффективности и при переписывании angular или knockout и вам от них нужна только функция выборки, а не фреймворк — почему нет?

                Может быть потому что я выигрываю в поддержке, мне проще найти разработчиков, знакомых с angular, а не с моей (или Вашей) поделкой. Angular протестирован, есть много людей, которые его поддерживают, огромный зоопарк компонентов готовых и т.д.

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


              1. mayorovp
                07.08.2018 10:13

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

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


  1. token
    07.08.2018 08:13
    -1

    Частичная оптимизация не даст ничего, поскольку jquery с его селекторами обычно используется не ради селекторов как таковых а ради того что можно сделать с элементами выбранными посредством них. Для примера $('.foobar').addClass('bla').removeClass('tutu'), ну заменим мы селектор на натив, а дальше то что? Либо циклы, либо опять заворачивать в jquery, либо писать собственную реализацию, тем самым на выходе получив jquery #2


    1. altrus Автор
      07.08.2018 09:07

      Разве в статье не написано про реализацию нужных методов?
      addClass -> el.classList.add(className);


      1. token
        07.08.2018 09:15

        Разве я говорил об этом?


        1. altrus Автор
          07.08.2018 09:27
          -1

          Не знаю. Я не понимаю, о чем вы говорите.


          1. Riim
            07.08.2018 14:58

            Я не понимаю, о чем вы говорите.

            Про циклы говорит:


            $('.foobar').addClass('bla')

            или


            Array.from(document.getElementsByClassName("foobar")).forEach(el => {
                el.classList.add('bla');
            });


            1. token
              07.08.2018 14:59

              В любом случае самым быстрым вариантом (увы и ах) будет простой советский for (var c = 0; c < els.length; c++) {… }


              1. Riim
                07.08.2018 15:03
                -1

                В 99% случаев больше потеряете на выросшем от таких конструкций размере бандла.


                1. token
                  07.08.2018 15:05

                  Возможно и так, но если говорить о размере бандла то document.querySelectorAll и document.getElementById — претендуют на первое место в раздувании размера )


    1. akeinhell
      07.08.2018 09:10

      уже все есть, зачем писать велосипеды?

      Array.from(document.querySelectorAll('a'))
          .forEach(el => {
             el.classList.add('test');
             el.classList.remove('test');
         })
      


      + меня бесит порядок аргументов в jquery в функциях ex.: each(index, element)


      1. token
        07.08.2018 09:14

        А вы попробуйте это прогнать по перформанса и посмотрите что будет быстрее


        1. dopusteam
          07.08.2018 09:23
          +1

          Для Вас так критична производительность? Зачем Вы изначально jquery в таком случае брали?


          1. token
            07.08.2018 09:33

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


            1. dopusteam
              07.08.2018 09:45

              Прошу прощения, вопрос автору адресован был, конечно же, просто веткой промахнулся как то


              1. token
                07.08.2018 09:47

                ок


              1. altrus Автор
                07.08.2018 10:20

                Я от нее и сейчас не отказываюсь


      1. srhbdv
        07.08.2018 09:28
        +1

        Зачем нужен Array.from?

        document.querySelectorAll('a').forEach(e => {
                e.classList.add('test')
                e.classList.remove('test')
            })


        1. token
          07.08.2018 09:43

          Шоб красиво было + по максимуму использовать весь доступный сахар.


        1. akeinhell
          07.08.2018 10:16

          developer.mozilla.org/ru/docs/Web/API/NodeList

          Однако некоторые старые браузеры на данный момент все еще не поддерживают NodeList.forEach().


          1. srhbdv
            07.08.2018 10:18

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


          1. srhbdv
            07.08.2018 10:27

            Array.from точно так же не поддерживается старыми браузерами.


            1. mayorovp
              07.08.2018 10:37

              Вот только Array.from можно заполифилить, а NodeList.forEach — не в любом браузере. Потому что Array — родной для JS объект, а NodeList — нативный.


              1. srhbdv
                07.08.2018 10:47

                Что за бред вы пишите. NodeList.forEach точно так же без проблем полифилится. И Array — это точно такой же нативный встроенный объект, как и NodeList.


                1. mayorovp
                  07.08.2018 10:52

                  Я не то слово сказал. Вот правильное: exotic object.


                  Если у прототипа NodeList не объявлен внутренний метод [[DefineOwnProperty]] — то никаким полифилом вы метод forEach ему не добавите. Это возможно, к примеру, в старых версиях IE где объекты DOM были COM-объектами.


                  1. srhbdv
                    07.08.2018 10:55
                    +1

                    Если мы будем рассматривать те самые старые версий IE, то полифилить NodeList.forEach не будет никакой необходимости, потому как и querySelector там нет, и много чего еще.

                    И если честно, уже не смешно в 2018ом году, ссылаться на старые версии IE.


      1. Bhudh
        07.08.2018 23:32

        Порядку аргументов есть извинение: в jQuery .each появился раньше, чем .forEach в Array.prototype.


  1. vlreshet
    07.08.2018 09:24
    +3

    Итого: мы выбрасываем jQuery, и пишем свой jQuery, только без тестов и кастрированный. Отлично. Ну и смысл?

    И вообще, если страница такая нагруженная что для неё каждая миллисекунда выигрыша это важно — то что вообще там jQuery делает?


    1. altrus Автор
      07.08.2018 09:30
      -3

      Ну и смысл?
      Смысл, наверное, в том, чтобы читать статью и пытаться понять, о чем она, а не выхватывать из нее знакомые слова, придумывать на лету свои фантазии, вытаскивать свои фобии и бороться с ними в комментариях.


      1. vlreshet
        07.08.2018 09:34
        +2

        Ну-ка перечислите фантазии в моём комментарии. Или фобии (вот это вообще супер).

        Вы предлагаете писать «свой jQuery» — это не фантазия, так и есть, вы предлагаете писать свою обёртку совместимую с jQuery.

        Без тестов — вы правда написали тесты для своей поделки? «прогнал руками в консоли» — не считается

        «Кастрированный» — ну очевидно же, тут вся суть в том что это будет неполноценный jQuery.

        Ну и так далее. Воспринимайте критику проще.


    1. token
      07.08.2018 09:32

      Присоединяюсь


  1. denis_skripnik
    07.08.2018 09:36

    Здравствуйте. Благодарю за статью — интересный подход. Интересно, можно ли подобным образом реализовать миграцию jquery ajax?


    1. vlreshet
      07.08.2018 09:38
      -1

      А почему нет? jQuery ajax же написан на JS. Можно спокойно под тот же fetch переписать. Любую функцию можно переписать вручную.


    1. token
      07.08.2018 09:40
      +1

      Нет, ну что вы, это нереально.


    1. altrus Автор
      07.08.2018 09:41
      -1

      А чем стандартный fetch или его полифил не устраивает?
      Смысл приведенной в статье миграции — чтобы не переписывать весь код, по селекторам он обычно довольно объемный. А AJAX вызовов обычно немного, перевести их на fetch не должно составить труда.

      // jQuery
      $(selector).load(url, completeCallback)
      
      // Native
      fetch(url).then(data => data.text()).then(data => {
        document.querySelector(selector).innerHTML = data
      }).then(completeCallback)


      1. vlreshet
        07.08.2018 09:47

        Оп, и у вас потерялись куки.


        1. altrus Автор
          07.08.2018 09:54

          с куками

          fetch(url, {
            credentials: "same-origin"
          }).then(data => data.text()).then(data => {
            document.querySelector(selector).innerHTML = data
          }).then(completeCallback)


          1. srhbdv
            07.08.2018 09:56
            -1

            Откройте для себя уже element.textContent, если вы так гонитесь за производительностью.


  1. stepmex
    07.08.2018 09:57

    Не помешало бы тесты сделать общедоступными, а то тестирования выглядит мягко говоря «сферическим котом в вакууме». Может вы там для jQuery скормили страницу на пол мегабайта, а JS ваш пример с тремя нодами.
    В добавок почему сравнение идет с jQuery 2.0? Последняя версия 3.3.1 и между 2 и 3 версией огромная разница в скорости.

    И примеры никудышные, попробуйте выбрать чистым JS вот такое:

    $('#form[name*="regsitr"] .field-wrap:eq(4) :checkbox:checked')


    jQuery выбирают для удобства разработки. Если вам нужна скорость, вы можете комбинировать стандартные функции и методы jQuery.

    Но все эти «переходы» сразу прекращаются когда речь заходит о событиях…


    1. srhbdv
      07.08.2018 10:02
      +2

      document.querySelectorAll('#form[name*="regsitr"] .field-wrap:nth-child(5) [type=checkbox]:checked')


      1. stepmex
        07.08.2018 11:35

        Чутка ошиблись:

        document.querySelectorAll('#form[name*="regsitr"] .field-wrap:nth-of-type(5) [type=checkbox]:checked')


    1. dom1n1k
      07.08.2018 10:16
      +2

      Справедливости ради: такие селекторы — вернейший признак говнокода.


  1. Hivemaster
    07.08.2018 10:12
    +2

    А теперь покажите код на чистом js для

    $('.some-node').wrap('<div class="wrapper"></div>')
                   .fadeOut(function(event) {
                       $(this).closest('.wrapper')
                              .slideUp();
                   });
    


    1. srhbdv
      07.08.2018 10:15

      Больше не делайте fadeOut на CPU, пожалуйста. Для этого давно есть css.


      1. Hivemaster
        07.08.2018 10:37

        Во-первых, jQuery для анимации использует requestAnimationFrame, если эта функция доступна, то есть тот же механизм, что и транзишены. Во-вторых, не подскажите чем делать fadeOut на 10-м IE?


        1. srhbdv
          07.08.2018 10:38

          Нет, requestAnimationFrame — это не тот же механизм, что транзишены.

          Во-вторых, не подскажите чем делать fadeOut на 10-м IE?
          Конечно подскажу. На css.


          1. Hivemaster
            07.08.2018 10:42

            Но как, если поддержка транзишенов появилась только в 11-м IE?


            1. srhbdv
              07.08.2018 10:44

              Нет, вы ошибаетесь. И css-transition и css-animation поддерживаются IE начиная с версии 10.


              1. Hivemaster
                07.08.2018 10:51
                -1

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


                1. altrus Автор
                  07.08.2018 11:28

                  Интересно, кто больше тормозит прогресс: Билл Гейтс со своим IE8, пользователи, которые на нем сидят, или разработчики, поддерживающие этих пользователей на их IE8


                  1. dom1n1k
                    07.08.2018 11:54
                    +1

                    Существует такое мнение, что IE11 будет существовать ещё очень-очень долго. Доля его стабилизируется на уровне пары процентов — вроде бы и немного, но не настолько, чтобы полностью ими пренебречь.
                    Причина — именно в его устаревших/маргинальных/нестандартных технологиях. У большинства они как заноза в заднице, но некоторым людям и компаниям они наоборот необходимы. И видели они ваш абстрактный прогресс в белых тапках, у них есть конкретные потребности.
                    В общем, ресурсы, рассчитанные на широкую аудиторию, вынуждены будут поддерживать IE ещё несколько лет.


                  1. TheDeadOne
                    07.08.2018 13:12

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


                    1. altrus Автор
                      07.08.2018 13:26
                      -1

                      Если заказчик понимает, что за те же деньги, но без поддержки рудиментов он может получить продукт с на 20% большей функциональностью, то вопросов нет. Это его выбор.


                      1. dom1n1k
                        07.08.2018 14:25
                        +1

                        Есть репутационные издержки, которые проще упредить, чем потом бороться с ними.
                        Например, я продаю водяные краны. Ко мне на сайт заходит какой-то клерк из ЖЭКа со своего старенького пентиума и видит полностью поломаный сайт. Вы наверное думаете, что он сразу же поймёт, что проблема в его браузере? Да щас. Он решит, что сайт хлам и как пить дать контора раздолбайская. Больше того, он потом ещё своему начальнику скажет, что эта контора (как там она называется? забыл. да и черт с ней) гавно.
                        Зато на сайте чистенький ES6, ога.


                        1. altrus Автор
                          07.08.2018 15:08

                          Во-первых, он половину сайтов видит в таком виде, и если не совсем дурак, то поймет, в чем дело (верней вы ему явно об этом большими буквами вверху страницы напишите)
                          Во-вторых, для таких случаев удобно делать отдельный шаблон с минимумом функциональности на дубовом HTML без шика. Наверно все CMS это позволяют (несколько шаблонов). Это опять же может быть проще, чем делать один шаблон для всех ПК.


                          1. krundetz
                            08.08.2018 16:08

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


                      1. krundetz
                        08.08.2018 15:53

                        В том то и дело, что на порядок большие деньги. Вы исходите только из затрат на сайт. А заказчик учитывает и затраты на другое программное обеспечение и затраты на оборудование и затраты на внедрение этого программного обеспечения и затраты на обучение персонала и возможно изменения бизнес-процессов. И бах таких рабочих мест может быть не 10 и даже не 100, а тысячи.


  1. dom1n1k
    07.08.2018 10:14
    +1

    В конструкторе класса желательно парсить sel, чтобы более эффективно применять querySelectorAll(), getElementsByClassName() и getElementById()
    Это прекрасно. Сколько оверхеда даст парсинг селектора?


    1. altrus Автор
      07.08.2018 11:12
      -1

      Думаете, простая работа со строками (разбить по пробелам, определить тип: тэг-класс-id) занимает больше времени, чем проходы по DOM-у?


      1. dom1n1k
        07.08.2018 11:38

        Получится что-то в таком духе? (код не тестировал, написано просто из головы)

        function $ (selector) {
        	const chunks = selector.trim().split(/\s+/);
        	if (chunks.length === 1) {
        		const firstChar = chunks[0].charAt(0);
        		const word = chunks[0].slice(1);
        		if (firstChar === '#') {
        			return document.getElementById(word);
        		}
        		else if (firstChar === '.') {
        			return document.getElementsByClassName(word);
        		}
        	}
        	else {
        		return document.querySelectorAll(selector);
        	}
        }

        Думаю, что да, это будет медленнее.

        А в реальности код будет ещё сложнее, потому что у меня не учтены по-нормальному атрибуты и ряд других нюансов.


  1. gnaeus
    07.08.2018 10:18

    Всю статью можно было заменить этой ссылкой: You Might Not Need jQuery


    Только вот внезапно окажется, что не вся функциональность jQuery пишется в одну-две строчки:


    // jQuery
    $('.inner').wrap('<div class="wrapper"></div>');
    
    // Native
    document.querySelectorAll('.inner').forEach(el => {
      const wrapper = document.createElement('div');
      wrapper.className = 'wrapper';
      el.parentNode.insertBefore(wrapper, el);
      el.parentNode.removeChild(el);
      wrapper.appendChild(el);
    });

    И если у нас реально много ручных DOM-манипуляций, то jQuery все еще остается хорошим выбором. Хотя лучше, конечно, взять какой-нибудь фреймворк из "большой тройки" :)


    1. mayorovp
      07.08.2018 10:38

      Поправка: el.parentNode.removeChild(el); можно не делать.


  1. altrus Автор
    07.08.2018 15:30

    -



  1. alex6636
    09.08.2018 13:55

    Наговнокодили, лишились кроссбраущерности, но выиграли пару наносекунд