Реализация и альтернатива основных JQuery функций на чистом JavaScript

в 15:14, , рубрики: ajax, dom, javascript, jquery

Когда я начинал учить веб-программирование, встретил лучший из всех, по моему мнению, фреймворков — JQuery. В то далёкое время нельзя было представить нормальное программирование без него, так как он мог делать одной строкой то, что делал JavaScript за 95.

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

Так вот, в этой статье я хочу рассказать о реализациях некоторых функций из JQuery на чистом JavaScript.

  1. $(document).ready( Function ); или $( Function );

    Для тех кто не знает, это функция готовности DOM дерева. Т.е. эта функция запускается, когда DOM страницы был полностью загружен.

    Начиная с IE9+ эту функцию можно заменить с помощью события DOMContentLoaded повешенного на document.

    Пример:

    document.addEventListener('DOMContentLoaded', function() {
       // Ваш скрипт
    }, false);

    Если вам нужна поддержка начиная с IE4+, то можно воспользоваться более старым методом — с помощью события readystatechange повешенного на document и проверкой readyState.

    Пример:

    document.onreadystatechange = function(){
       if(document.readyState === 'complete'){
          // Ваш скрипт
       }
    };

    Если же мы посмотрим в исходники JQuery, то выйдет следующая функция:

    Пример:

    var ready = (function() {
      var readyList,
          DOMContentLoaded,
          class2type = {};
          class2type["[object Boolean]"] = "boolean";
          class2type["[object Number]"] = "number";
          class2type["[object String]"] = "string";
          class2type["[object Function]"] = "function";
          class2type["[object Array]"] = "array";
          class2type["[object Date]"] = "date";
          class2type["[object RegExp]"] = "regexp";
          class2type["[object Object]"] = "object";
    
      var ReadyObj = {
          // Является ли DOM готовым к использованию? Установите значение true, как только оно произойдет.
          isReady: false,
          // Счетчик, чтобы отслеживать количество элементов, ожидающих до начала события. См. #6781
          readyWait: 1,
          // Удерживать (или отпускать) готовое событие
          holdReady: function(hold) {
            if (hold) {
              ReadyObj.readyWait++;
            } else {
              ReadyObj.ready(true);
            }
          },
          // Обрабатывать, когда DOM готов
          ready: function(wait) {
            // Либо трюк не работает, либо событие DOMready/load и еще не готовы
            if ((wait === true && !--ReadyObj.readyWait) || (wait !== true && !ReadyObj.isReady)) {
              // Убедитесь, что тело существует, по крайней мере, в случае, если IE наложает (ticket #5443).
              if (!document.body) {
                return setTimeout(ReadyObj.ready, 1);
              }
    
              // Запоминаем что DOM готов
              ReadyObj.isReady = true;
              // Если обычное событие DOM Ready запускается, уменьшается и ожидает, если потребуется,
              if (wait !== true && --ReadyObj.readyWait > 0) {
                return;
              }
              // Если функции связаны, выполнить
              readyList.resolveWith(document, [ReadyObj]);
    
              // Запуск любых связанных событий
              //if ( ReadyObj.fn.trigger ) {
              //    ReadyObj( document ).trigger( "ready" ).unbind( "ready" );
              //}
            }
          },
          bindReady: function() {
            if (readyList) {
              return;
            }
            readyList = ReadyObj._Deferred();
    
            // Поймать случаи, когда $(document).ready() вызывается после
            // события браузера, которое уже произошло.
            if (document.readyState === "complete") {
              // Обращайтесь к нему асинхронно, чтобы позволить скриптам возможность задержать готовность
              return setTimeout(ReadyObj.ready, 1);
            }
    
            // Mozilla, Opera и webkit nightlies в настоящее время поддерживают это событие
            if (document.addEventListener) {
              // Используем удобный callback события
              document.addEventListener("DOMContentLoaded", DOMContentLoaded, false);
              // Откат к window.onload, который всегда будет работать
              window.addEventListener("load", ReadyObj.ready, false);
    
              // Если используется тип событий IE
            } else if (document.attachEvent) {
              // Обеспечить запуск перед загрузкой,
              // Возможно, поздно, но безопасно также для iframes
              document.attachEvent("onreadystatechange", DOMContentLoaded);
    
              // Откат к window.onload, который всегда будет работать
              window.attachEvent("onload", ReadyObj.ready);
    
              // Если IE, а не frame
              // Постоянно проверяем, готов ли документ
              var toplevel = false;
    
              try {
                toplevel = window.frameElement == null;
              } catch (e) {}
    
              if (document.documentElement.doScroll && toplevel) {
                doScrollCheck();
              }
            }
          },
          _Deferred: function() {
            var // список callback
              callbacks = [],
              // stored [ context , args ]
              fired,
              // Чтобы избежать запуска, когда это уже сделано
              firing,
              // Чтобы узнать, отменена ли отсрочка
              cancelled,
              // Отложенный
              deferred = {
                // done( f1, f2, ...)
                done: function() {
                  if (!cancelled) {
                    var args = arguments,
                      i,
                      length,
                      elem,
                      type,
                      _fired;
                    if (fired) {
                      _fired = fired;
                      fired = 0;
                    }
                    for (i = 0, length = args.length; i < length; i++) {
                      elem = args[i];
                      type = ReadyObj.type(elem);
                      if (type === "array") {
                        deferred.done.apply(deferred, elem);
                      } else if (type === "function") {
                        callbacks.push(elem);
                      }
                    }
                    if (_fired) {
                      deferred.resolveWith(_fired[0], _fired[1]);
                    }
                  }
                  return this;
                },
    
                // Разрешить с заданным контекстом и аргументами
                resolveWith: function(context, args) {
                  if (!cancelled && !fired && !firing) {
                    // Убедитесь, что имеются аргументы (#8421)
                    args = args || [];
                    firing = 1;
                    try {
                      while (callbacks[0]) {
                        callbacks.shift().apply(context, args); //shifts a callback, and applies it to document
                      }
                    } finally {
                      fired = [context, args];
                      firing = 0;
                    }
                  }
                  return this;
                },
    
                // решить с этим в качестве контекста и приведенных аргументов
                resolve: function() {
                  deferred.resolveWith(this, arguments);
                  return this;
                },
    
                // Отложено ли это решение?
                isResolved: function() {
                  return !!(firing || fired);
                },
    
                // Отмена
                cancel: function() {
                  cancelled = 1;
                  callbacks = [];
                  return this;
                }
              };
    
            return deferred;
          },
          type: function(obj) {
            return obj == null ?
              String(obj) :
              class2type[Object.prototype.toString.call(obj)] || "object";
          }
        }
        // Проверка готовности DOM для Internet Explorer
      function doScrollCheck() {
        if (ReadyObj.isReady) {
          return;
        }
    
        try {
          // Если используется IE, то используйте трюк Диего Перини
          // http://javascript.nwbox.com/IEContentLoaded/
          document.documentElement.doScroll("left");
        } catch (e) {
          setTimeout(doScrollCheck, 1);
          return;
        }
    
        // И выполнить функцию ожидания
        ReadyObj.ready();
      }
      // Функция очистки для document ready
      if (document.addEventListener) {
        DOMContentLoaded = function() {
          document.removeEventListener("DOMContentLoaded", DOMContentLoaded, false);
          ReadyObj.ready();
        };
    
      } else if (document.attachEvent) {
        DOMContentLoaded = function() {
          // Убедимся, что тело существует, по крайней мере, в случае, если IE наложает (ticket #5443).
          if (document.readyState === "complete") {
            document.detachEvent("onreadystatechange", DOMContentLoaded);
            ReadyObj.ready();
          }
        };
      }
    
      function ready(fn) {
        // Прикрепление слушателя
        ReadyObj.bindReady();
    
        var type = ReadyObj.type(fn);
    
        // Добавление callback'а
        readyList.done(fn); // ReadyList является результатом _Deferred()
      }
      return ready;
    })();

    Запуск функции происходить таким образом:

    ready(function() {
       // Ваш скрипт
    });

  2. $.ajax( Object );

    Для тех кто не знает, эта функция выполняет асинхронный HTTP (Ajax) запрос.

    Как бы это ни было банально, но альтернативой для Jquery.ajax() является XMLHttpRequest

    Немного об использовании:

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

    function getXmlHttp(){
      var xmlhttp;
      try {
        xmlhttp = new ActiveXObject('Msxml2.XMLHTTP');
      } catch (e) {
        try {
          xmlhttp = new ActiveXObject('Microsoft.XMLHTTP');
        } catch (E) {
          xmlhttp = false;
        }
      }
      if (!xmlhttp && typeof XMLHttpRequest !== 'undefined') {
        xmlhttp = new XMLHttpRequest();
      }
      return xmlhttp;
    }

    А вот сам пример стандартного POST запроса с обработкой ошибок:

    var xmlhttp = getXmlHttp(); // Получаем нашу функцию
    xmlhttp.open('POST', '/someurl', true); // Отправляем POST запрос на адрес "/someurl" 
    
    // Вызываем функцию при изменении статуса запроса
    xmlhttp.onreadystatechange = function(){
      if (xmlhttp.readyState !== 4) return; // Если запрос не завершён, то ничего не делаем
      // Немного о статусах:
      // Статус 0 — Объект XMLHttpRequest был создан, но метод open() ещё не вызывался.
      // Статус 1 — Был вызван метод open(). На этом этапе методом setRequestHeader() могут быть установлены заголовки запроса (request headers), после чего, для начала выполнения запроса, может быть вызван метод send() .
      // Статус 2 — Был вызван метод send() и получены заголовки ответа (response headers) .
      // Статус 3 — Получена часть ответа. Если responseType это пустая строка или имеет значение "text", responseText будет содержать загруженную порцию текста ответа.
      // Статус 4 — Операция доставки данных завершена. Это может означать как то, что передача данных полностью завершена успешно, так и то, что произошла ошибка.
      clearTimeout(timeout); // Удаляем Timeout, если запрос завершён
    
      if (xmlhttp.status == 200) {
          // Если запрос был отправлен успешно и мы получили ответ, то обрабатываем информацию
          console.log(xmlhttp.responseText);
      } else {
          // Если же у нас ошибка, то отправляем её в обработчик
          handleError(xmlhttp.statusText);
      }
    }
    
    // Указываем данные, которые нам нужно отправить
    xmlhttp.send("a=5&b=4");
    // Создаём таймер на 10 секунд. Он нужен для того, чтобы, когда ответ от сервера не приходит, выдать ошибку
    var timeout = setTimeout( function(){ xmlhttp.abort(); handleError('Time over') }, 10000);
    
    // Создаём функцию обработки ошибок
    function handleError(message) {
      // Тут мы принимает текст ошибки и распологаем ним как хотим
      console.log('Ошибка: ' + message);
    }

    По такой же технологией сделана функция JQuery AJAX.

  3. Глобальная функция $(...);

    Если кто не знает, этой функцией создаётся глобальный JQuery объект.

    Тут я не буду расписывать полный функционал этой функции, так как это займёт у меня минимум неделю, а просто напишу, как создаётся подобная вещь по примеру JQuery.

    Для начала создадим обычную функцию, к примеру, Library с аргументами (селектор и контекст).

    var Library = function (selector, context) {};

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

    var init = function (selector, context) {
        // Для начала мы создадим массив, 
        // в который будем скидывать элементы
        var array = [];
    
        /** Сначала мы проработаем вариан,
         * когда кодер указал селектор 
         * или HTML код в первом аргументе
         */
        if (typeof selector === 'string' ) {
            /** Нам нужно попарсить HTML код
             * чтобы узнать селектор это или код.
             * Для парсинка в JQuery используется
             * следующее регулярное выражение:
             * /^(?:s*(<[wW]+>)[^>]*|#([w-]+))$/
             */
            if (/^(?:s*(<[wW]+>)[^>]*|#([w-]+))$/.exec(selector)) {
                // Сначала я распарсю код
                var DOM = new DOMParser().parseFromString(selector, 'text/html');
                var DOMList = DOM.body.childNodes;
    
                // Тут ещё нужно вспомнить про context и проверить его на значения
                if (!context || {}.toString.call(context) !== '[object Object]') {
                    context = null;
                };
    
                // Далее добавляем элементы новый в массив
                for (var i = 0; i < DOMList.length; i++) {
                    if (context) {
                        for (var attr in context) {
                            DOMList[i].setAttribute(attr, context + '');
                        };
                    };
                    
                    array[array.length] = DOMList[i];
                };
    
                return array;
            } else {
                // Тут нужно проверить
                // является ли свойство
                // context элементом,
                // в котором нужно искать
                // объект
                var DOMList = {}.toString.call(context) === '[object HTMLElement]' ? context.querySelectorAll(selector) : document.querySelectorAll(selector);
    
                // Теперь перекидываем все элементы в массив
                // и выводим
                for (var i = 0; i < DOMList.length; i++) {
                    array[array.length] = DOMList[i];
                };
    
                return array;
            };
        // Тут мы проверим, является ли первый аргумент массивом
        } else if ({}.toString.call(selector) === '[object Array]') {
            // Перекидываем значения и выводим
            for (var i = 0; i < selector.length; i++) {
                array[array.length] = selector[i];
            };
    
            return array;
        // Далее я проверю, может это объект или один элемент
        } else if ({}.toString.call(selector) === '[object Object]' || {}.toString.call(selector) === '[object HTMLElement]') {
            // Запихиваем объект в массив и выводим
            array[0] = selector;
    
            return array;
        // Теперь проверяем, может это живой массив элементов?
        } else if ({}.toString.call(selector) === '[object HTMLCollection]' || {}.toString.call(selector) === '[object NodeList]') {
            // Перекидываем значения и выводим
            for (var i = 0; i < selector.length; i++) {
                array[array.length] = selector[i];
            };
    
            return array;
        // Если ничего не подходит, то выводим пустой массив
        } else {
            return array;
        }
    };

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

    var Library = function (selector, context) {
        // Получаем массив из основной функции
        var array = new init(selector, context);
        /** Тут мы создаём объект.
         * К его proto присваиваем
         * прототип главной функции,
         * чтобы потом можно было 
         * создавать дополнительный
         * функционал
         */
        var object = {
            __proto__: Library.prototype
        };
    
        // Далее мы перекидываем элементы
        // Из массива в объект и создаём
        // параметр length, чтобы потом можно
        // было узнать, сколько элементов
        // в объекте
        for (var i = 0; i < array.length; i++) {
            object[i] = array[i];
        };
        object.length = array.length;
    
        return object;
    };

    Вот и готово. Теперь мы можем получить массив элементов через функцию Library(...); и создавать дополнительный функционал через такую конструкцию Library.prototype.myFunction = function () {...};

Пока всё. Через время я буду опубликовывать статьи про функции JQuery более конкретно

Автор: Yuri Spivak

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js