18 неожиданностей при чтении исходного кода jQuery

http://quickleft.com/blog/18-surprises-from-reading-jquery-s-source-code
  • Перевод
Перевод статьи «18 Surprises From Reading jQuery's Source Code», David Aragon.

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

Примечание: Я использую синтаксис $.fn.method() чтобы указать вызов method на выборке элементов. Например, когда я пишу $.fn.addClass, это обозначает использования как $(‘div’).addClass(‘blue’) или $(‘a.active’).addClass(‘in-use’). $.fn это прототип для jQuery оберток элементов.

  1. Вес Sizzle: Sizzle это движок jQuery для выборок из DOM по CSS селекторам. Это то что превращает $(‘div.active’) в массив элементов, которыми вы можете оперировать. Я знал, что Sizzle составляет большу́ю часть jQuery, но я был удивлен узнав, насколько он огромен, на самом деле. Это самая большая фича, в плане количества строк, в исходном коде jQuery. По моим расчетам он составляет 22% всей кодовой базы. Это затмевает следующую по величине вещь в jQuery — $.ajax, который занимает всего лишь 8% кода библиотеки.

  2. $.grep: Этот метод похож на Underscore метод _.filter, он также принимает два аргумента, массив элементов и функцию, и возвращает элементы, которые прошли тест функции.

  3. Bubbling предостережения: jQuery запрещает всплывание(bubbling) одного типа событий. Это событие load. Внутри, jQuery передает специальный флаг noBubble: true, для всех load событий, так чтобы image.load события не могли всплывать вверх к объекту window (которые могут ошибочно восприниматься, как window.load).

  4. Дефолтная скорость анимации: jQuery анимирует элементы изменяя их стили в быстрой последовательности. Каждое из этих быстрых изменений называется “тик(tick)”. Скорость по умолчанию для анимации, это тик каждые 13 миллисекунд, вы можете настроить это, переопределив jQuery.fx.interval на свое собственное число.

  5. $.fn.addClass принимает функцию: Мы обычно передаем строку с именами классов в функцию $.fn.addClass. Но она также принимает функцию. Переданная функция должна возвращать строку с именами классов разделенными пробелом. В качестве бонуса, эта функция принимает индекс элемента из списка элементов, к которым она применяется, что позволяет вам строить умные имена классов.

  6. То же может и $.fn.removeClass: Этот метод также принимает функцию, как в методе выше. Переданная функция также принимает индекс элемента.

  7. Псевдо-селектор :empty Этот удобный псевдо-селектор выбирает элементы без дочерних узлов.

  8. Псевдо-селекторы :lt и :gt: Эти псевдо-селекторы выбирают элементы по их индексу в выборке. Например, $(‘div:gt(2)’) возвратит все div'ы, кроме первых трех (индексация начинается с нуля). Если вы передадите в аргумент негативное число, JQuery посчитает обратно с конца выборки.

  9. $(document).ready() использует обещания(promises): Оказывается, что jQuery использует свой собственный продукт. Внутри, старого доброго $(document).ready() используется jQuery deferred, для определения, когда DOM полностью загружен.

  10. $.type: Я уверен, вы все знакомы с использованием typeof, но знали ли вы, что jQuery предоставляет метод .type()? jQuery версия более разумна, чем нативная браузерная версия. Например, typeof (new Number(3)) возвращает “object,” тогда как $.type(new Number(3)) возвращает “number.” Добавлено: Как ShirtlessKirk указал в комментариях, $.type возвращает тип возврата метода .valueOf(). Так что правильнее сказать, что $.type скажет вам тип возвращаемого значения объекта.

  11. $.fn.queue: Вы можете получить очередь эффектов элемента, при помощи следующего кода: $(‘div’).queue(). Это полезно, если вам нужно узнать, сколько эффектов еще должны быть применены к элементу. Даже более полезно самому добавлять эффекты в очередь. Из доков jQuery:

    $( document.body ).click(function() {
    $( "div" )
      .show( "slow" )
      .animate({ left: "+=200" }, 2000 )
      .queue(function() {
        $( this ).addClass( "newcolor" ).dequeue();
      })
      .animate({ left: "-=200" }, 500 )
      .queue(function() {
        $( this ).removeClass( "newcolor" ).dequeue();
      })
      .slideUp();
    });
    

  12. Click события запрещены на неактивных (disabled) элементах: jQuery не обрабатывает события клика по отключенным элементам, хорошая оптимизация, которая снимает с вас обязанность проверять это самому.

  13. $.fn.on принимает объект: А вы знали, что $.fn.on принимает объект для подключения к множеству событий одновременно? Пример из доков jQuery:

    $( "div.test" ).on({
    click: function() {
      $( this ).toggleClass( "active" );
    }, mouseenter: function() {
      $( this ).addClass( "inside" );
    }, mouseleave: function() {
      $( this ).removeClass( "inside" );
    }
    });
    

  14. $.camelCase: Этот вспомогательный метод превращает дефис-строки в camelCase.

  15. $.active: Вызов $.active возвращает количество активных XHR запросов. Это может пригодиться для ограничения количества одновременно активных AJAX вызовов.

  16. $.fn.parentsUntil / $.fn.nextUntil / $.fn.prevUntil: Я хорошо знаком с методами .parents(), .next() и .prev(), но я не знал о существовании Until версий этих методов. По существу, эти методы будут отбирать parents/next/prev элементы, пока не дойдут до переданного остановочного элемента.

  17. Аргументы $.fn.clone: Когда вы клонируете элемент, вы можете также клонировать его data атрибуты и события, передав true первым аргументом в метод clone.

  18. Другие аргументы $.fn.clone: В дополнение, вы можете клонировать data атрибуты и события дочерних элементов, передав true во второй аргумент. Это называется «глубинное клонирование». Второй аргумент по умолчанию равен первому (который, по умолчанию, равен false). Таким образом, если первый аргумент true и вы хотите, чтобы второй был равен true, вы можете опустить второй аргумент.


В более ранней версии этой статьи было сказано, что возвращаемое значение функции-аргумента из примера №5 должна быть строка разделенная запятыми. Оказывается, это должна быть строка разделенная пробелами. Спасибо, Джонатан!
Поделиться публикацией
Комментарии 37
    +3
    Отдельное спасибо за «jQuery.fx.interval»
      0
      Всегда пожалуйста)
      +6
      Пункты 5 и 6, вроде как, описаны в документации. Правда на деле ни разу не приходилось передавать туда функцию.

      Пункт 7 — это обычный селектор CSS, как бы он по логике и должен быть в jQuery.

      Насчёт пункта 12…
      Вроде как атрибут disabled=«disabled» не всегда игнорировался.
      Насколько я помню, мне как раз и приходилось писать костыли, чтобы при клике на ссылку или DIV проверять, есть ли disabled или нет.
      Проще говоря, событие всё же вызывалось.

      Пункты 17 и 18 тоже описаны в документации.

      — >>и хотя я считаю себя продвинутым JavaScript разработчиком
      Хороший такой специалист :)

      >> не читал исходники jQuery с начала и до конца
      К слову, я тоже не читал.
      Уверен, что прочитав мануал на сайте jQuery, можно ещё больше узнать интересных вещей и поведений.

      P.S.:
      А так…
      Когда-то давно прочитал в одной из статей о jQuery, зачем в функцию вставляется аргумент с названием undefined.
      И теперь всегда использую обрамление скриптов следующим кодом: (function(window, document, undefined){}(document, jQuery)),
        0
        Когда-то давно прочитал в одной из статей о jQuery, зачем в функцию вставляется аргумент с названием undefined.

        Потому что undefined, который на самом деле переменная window.undefined, может быть перекрыт. А в вашем примере self-invoking function, код внутри начинает использовать undefined параметр, переданный в функцию, который неожиданно без инициализации имеет значение примитивного типа undefined.
          +1
          undefined передают как переменную внутрь просто ради профита от ее переименования в более короткий вариант в процессе сжатия исходников :}
            0
            тоже верно, хотя сам предпочитаю не передавать, не так уж часто undefined используется в чистом виде
              0
              (function(undefined){console.log(undefined);})(17)
              
                0
                Ничего себе!
                +1
                Неправда. undefined объявляют аргументом именно за тем, чтобы получить «настоящую» неопределенную переменную.
                  0
                  попробуйте переопределить undefined в браузерах, которые поддерживает jquery 2, а потом посмотрите что

                  (function() { console.log(undefined); })();
                  после минификации останется таким же

                  а вот
                  (function(undefined) { console.log(undefined); })();
                  станет каким-нибудь вот таким:
                  (function(a) { console.log(a); })();

                  профит очевиден, а за размером в jquery следят ололо как
                    +1
                    Эта техника использовалась в библиотеках за долго до jQuery2. Бонус после минификации — всего лишь сторонний эффект.
                      0
                      да, видимо так и есть, спасибо
                      0
                      и тут я открыл исходники jquery 2 и увидел что там больше так не делают.
                      видимо, я и правда был не прав и минификация — лишь побочный плюс :{
                  +1
                  «In modern browsers (JavaScript 1.8.5 / Firefox 4+), undefined is a non-configurable, non-writable property per the ECMAScript 5 specification» (тут).
                  0
                  По поводу :empty.
                  В CSS :empty срабатывает только если в теге нет ни вложенных тегов, ни символов. Даже <td>& nbsp;</td> не подцепится через :empty
                    +2
                    ну так какой же это empty?

                    Вот то, что он пробельные символы(незначащие для HTML) не опознаёт как empty — нехорошо:
                       $('<ul>\n</ul>').is(':empty');
                    false
                       $('<ul></ul>').is(':empty');
                    true
                    
                      +2
                      Всё ок, вот описание соответствующего псевдокласса на MDN:

                      The :empty pseudo-class represents any element that has no children at all. Only element nodes and text (including whitespace) are considered. Comments or processing instructions do not affect whether an element is considered empty or not.

                      + демо: codepen.io/anon/pen/LcJtC
                        +3
                        В селекирах CSS 4-го уровня описывается псевдо-класс :blank, который будет ловить элементы с пробельными символами — dev.w3.org/csswg/selectors4/#the-blank-pseudo.
                    +3
                    Обрамление скриптов позабааило))) А так, ну да одна из реализаций области видимости с помощью самовызывающейся функции, только вопрос зачем вы переопределили window как document, а document как jQuery? Либо вы не понимаете что написали, либо я не понимаю зачем вам это.
                    +5
                    $.grep: Этот метод похож на Underscore метод _.filter, он также принимает два аргумента, массив элементов и функцию, и возвращает элементы, которые прошли тест функции.
                    Для полноты картины уместно упомянуть (и упоминаю), что оба они служат костылями для браузеров, не имеющих поддержки метода Array.prototype.filter то есть, честно говоря, только для Internet Explorer восьмой версии и более ранних версий его.

                    Проще программировать с использованием этого метода, а старые IE подпирать посредством «es5-shim» с сохранением синтаксиса.
                      +3
                      Сразу уточню, что здесь «честно говоря» означает «честно оценивая современную ситуацию». (Строго-то говоря, у Firefox 1.5 также не было поддержки Array.prototype.filter но эта версия Файерфокса вымерла, тогда как IE8 преспокойно продолжает крутиться в Windows XP.)
                        +3
                        Да, Array.prototype.filter рулит! Я уже и забыл о всяких дополнительных велосипедах:)
                          +4
                          Справедливости ради надо сказать, что _.filter() работает с любыми коллекциями (а не только с массивами), и временами это весьма удобно.
                            0
                            Array.from(коллекция).filter(…)
                              0
                              Не работает для объектов:

                              var collection = {
                                      foo: 'bar',
                                      baz: 'qux'
                              };
                              
                              var predicate = function (x) {
                                      return x === 'bar';
                              };
                              
                              console.log(Array.from(collection).filter(predicate)); // []
                              console.log(_.filter(collection, predicate)); // [ 'bar' ]
                              console.log(Array.prototype.filter.call(collection, predicate));
                              
                              +1
                              Можно Array.prototype.filter.call для array-like объектов использовать:

                              var arrayLikeObj = {
                                0: 42,
                                1: 'foo',
                                2: 101,
                                3: 300,
                                4: 'bar',
                                5: 'baz',
                                6: Infinity,
                                7: 'qux',
                                length: 8
                              };
                              
                              console.log(Array.prototype.filter.call(
                                arrayLikeObj,
                                function(item) {
                                  return typeof item == 'number';
                                }
                              ));
                              // [42, 101, 300, Infinity] 
                              
                                –1
                                см. коммент выше: не работает для объектов
                                  +1
                                  Я написал что для array-like объектов, т.е. для объектов с полями-индексами и length. Или arguments, например :)
                                  Для обычных объектов, разумеется, нет. Хотя для примера из вашего комментария можно соорудить что-то вроде этого:

                                  var collection = {
                                    foo: 'bar',
                                    baz: 'qux'
                                  };
                                  
                                  var predicate = function (x) {
                                    return collection[x] === 'bar';
                                  };
                                  
                                  console.log(Array.prototype.filter.call(Object.keys(collection), predicate).map(function(key) { return collection[key]; }));
                                  


                                  Но это уже легкий изврат :)
                                  Я в таких случаях предпочитаю писать более прямой и понятный код. По крайней мере поведение _.filter в данном случае мне кажется не совсем очевидным, поскольку фильтрующая функция применяется к значениям, а не к ключам объекта или хотя бы к парам [ключ, значение]. Но это ИМХО, конечно.
                                    0
                                    Нет, я знаю, как работает Array.prototype.filter(), и так же знаю, как работает _.filter(), и просто напоминаю про разницу (чтоб не сложилось по первому комменту мнения, что это идентичные функции.
                            +6
                            Стоит отметить, что часть описанного выше есть в документации, как верно отметил zalatov_a, недокументированные же функции очень не рекомендуются к использованию авторами jquery, т.к. если их не задокументировали, то скорее всего были причины и авторы jQuery оставляют за собой возможность делать в них изменения без каких-бы то ни было предупржлений и уведомлений, что может подарить вам прекрасные ночи в обнимку с дебаггером.

                            Поэтому читать про такие фишки — интересно, знать — полезно, использовать — на свой страх и риск.
                              –5
                              Мы обычно передаем строку с именами классов в функцию $.fn.addClass. Но она также принимает функцию. Переданная функция должна возвращать строку с именами классов разделенными пробелом.


                              Эээ… и что тут «удивительного»? Для JS это очень нормально — помещать функции в аргументы — лишь бы возвращали ожидаемое.
                                +1
                                В 13м пункте можно еще и делегирование событий делать

                                $(".parent").on({
                                    click: function() {
                                        $( this ).toggleClass( "active" );
                                    }, mouseenter: function() {
                                        $( this ).addClass( "inside" );
                                    }, mouseleave: function() {
                                        $( this ).removeClass( "inside" );
                                    }
                                }, '.children'); // <- от тут
                                
                                  0
                                  Жаль не $(".parent").on(".children", {...}); хотя, там и так аргументами жонглируют как сумасшедшие…
                                    0
                                    Что первый вариант, что второй вариант мне кажется… странным… и не совсем понятным.

                                    Наверно, всё дело в том, что примеры неправильные.
                                    Надо не с CSS-селекторами оперировать, а с переменными или объектами типа document, window.

                                    Ведь, общепринято использовать $('.parent .child').on(...);

                                    А вот если $(document).on($link, ...), тогда да…
                                      0
                                      $link в данном случае все-равно должен быть селектором, а на document, по возможности, лучше не вешать.

                                      Но дело в другом, если перечень событий и колбеков большой, можно запутаться, хотя js разработчики давно привыкли читать с начала и с конца, а потом посередине (:
                                        0
                                        можно запутаться

                                        Чтобы не путаться извращенцы типа меня пишут код немного иначе:

                                        var onclick = function(){ $(this).toggleClass("active");}
                                        var onenter = function(){ $(this).addClass("inside");}
                                        var onleave = function(){ $(this).removeClass("inside");}
                                        var binds = {
                                          click: onclick,
                                          mouseenter: onenter,
                                          mouseleave: onleave
                                        };
                                        $(".parent").on(binds, '.children');
                                        

                                        При небольшом списке "binds" может быть лишним, но функции в аргументе-объекте — это ад для читателя.

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

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое