Реализация, аналог и адаптация для «чистого» JavaScript’а JQuery функции JQuery(); и прилегающие к ней

в 17:43, , рубрики: dom, javascript, jqeury, jquery

Когда-то библиотека jQuery была хорошим помощником для программистов. Так как она позволяла в разы облегчить создание функционала, который в то время с помощью одного JavaScript было почти нереально написать, и предоставляла очень хорошую кроссбраузерность.

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

В этой статье я расскажу про самую главную функцию jQuery — jQuery();.

Функция JQuery(); состоит из двух функций — основной и так называемой обложки. Вы запускаете обложку, обложка запускает основную функцию и возвращает из неё результат.

Обложка выглядит следующим образом:

var jQuery = function (selector, context) {
   return new jQuery.prototype.init(selector, context);
};

Как видно из кода, когда мы запускаем функцию jQuery(); она запускает функцию init с теми же параметрами из своего прототипа. Именно функция init является основной.

Далее нам следует создать прототип для нашей функции, в конструкторе которой мы будем ссылаться на функцию jQuery. Заметка: в старых версиях jQuery, параметр constructor не указывается.

jQuery.prototype = {
   constructor: jQuery
};

Откуда взялось свойство jQuery.fn и почему мы можем указывать новые функции для jQuery объектов через него?
Дело в том, что разработчики приравняли объект jQuery.prototype и jQuery.fn, тем самым задав для двух параметров один объект, чтобы иметь возможность более кратко добавлять новые функции для объектов.

jQuery.fn = jQuery.prototype = {
   constructor: jQuery
};

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

У jQuery вся функция состоит из условий, которые перебирают данные полученные функцией, и массива, в который скидываются нужные элементы.

Начальный вид функции выглядит так:

var init = jQuery.prototype.init = function (selector, context) {
   /** Для начала создаём массив, в который будем скидывать нужные элементы */
   var array = [];
   /** Сначала мы проработаем вариан,
    * когда кодер указал селектор 
    * или HTML код в первом аргументе
    */
   if (typeof selector === 'string') {
   /** Далее проверяем, не является ли первый аргумент DOM элементом */
   } else if (selector.nodeType) {
   /** 
    * Следующая проверка будет на ситуацию
    * если первый аргумент является массивом
   */
   } else if ({}.toString.call(selector) === '[object Array]') {
   /**
    * Следующей проверкой будет вариант,
    * когда первый аргумент является объектом
    */
   } else if ({}.toString.call(selector) === '[object Object]') {
   /** Теперь проверяем, может это живой массив элементов? */
   } else if ({}.toString.call(selector) === '[object HTMLCollection]' || {}.toString.call(selector) === '[object NodeList]') {
   /** Если первый аргумент — это функция */
   } else if ({}.toString.call(selector) === '[object Function]') {
   /** Если же ни однин тип не подошёл, то выводим пустой массив */
   } else {
      return array;
   }
};

Рассмотрим первое условие — если селектор является строкой. Если это строка, то нам следует проверить: это селектор или HTML код (ведь jQuery таким образом может парсить HTML код).

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

/** Проверяем, не является ли селектор HTML кодом */
if (selector[0] === "<" && selector[selector.length - 1] === ">" && selector.length >= 3) {
/** Если это не HTML, то обычный селектор */
} else {        
};

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

  1. С помощью специальных функций, парсится HTML код и живой массив, который вернула функция парсинга, объединяется с основным массивом.
    Реализация и аналог функции парсинга HTML кода — jQuery.parseHTML()

    К сожалению, я не расскажу про то, как реализована эта функция, т.к. у неё слишком много ответвлений, про которые можно писать отдельную статью. Но вы не расстраивайтесь, про то, как работает эта функция, уже написали. Вы можете прочитать эту статью перейдя по следующей ссылке: habrahabr.ru/post/164533

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

    1. Первая альтернатива — создадим элемент, в который с помощью innerHTML внесём нашу строку с кодом и заберём уже готовые DOM элементы.
      var parseHTML = function (HTML) {
         /**
          * Хоть мы и пишем простую альтернативу,
          * но проверить тип аргумента и
          * проверить это код или нет — нужно.
          * Код это или нет узнаём тем же образом
          * что и в условии основной функции
          */
         if (typeof HTML === 'string' && HTML[0] === "<" && HTML[HTML.length - 1] === ">" && HTML.length >= 3) {
            /** Создаём элемент */
            var E = document.createElement('div');
            /** Вставляем в элемент наш HTML код */
            E.innerHTML = HTML;
            /** Далее выводим готовый живой массив */
            return E.childNodes;
         } else {
            /** Если тип — не строка, то вернём пустой массив */
            return [];
         };
      };

    2. Вторая альтернатива — распарсить с помощью функции DOMParser(); и её дополнительной функции parseFromString();.
      var parseHTML = function (HTML) {
         /**
          * Хоть мы и пишем простую альтернативу,
          * но проверить тип аргумента и
          * проверить это код или нет — нужно.
          * Код это или нет узнаём тем же образом
          * что и в условии основной функции
          */
         if (typeof HTML === 'string' && HTML[0] === "<" && HTML[HTML.length - 1] === ">" && HTML.length >= 3) {
            /** Создаём новый HTML документ с кодом из аргумента  */
            var DOM = new DOMParser().parseFromString(HTML, 'text/html');
            /** Получаем наше пропарсенное содержимое  */
            var DOMList = DOM.body.childNodes;
            /** Далее выводим готовый живой массив  */
            return DOMList;
         } else {
            /** Если тип — не строка, то вернём пустой массив  */
            return [];
         };
      };

    3. Третья альтернатива — распарсить с помощью функции document.implementation.createHTMLDocument();.
      var parseHTML = function (HTML) {
         /**
          * Хоть мы и пишем простую альтернативу,
          * но проверить тип аргумента и
          * проверить это код или нет — нужно.
          * Код это или нет узнаём тем же образом
          * что и в условии основной функции
          */
         if (typeof HTML === 'string' && HTML[0] === "<" && HTML[HTML.length - 1] === ">" && HTML.length >= 3) {
            /** Создадим новый HTML документ */
            var DOM = document.implementation.createHTMLDocument();
            /** Внесём в него HTML строку */
            DOM.body.innerHTML = HTML;
            /** Получаем уже живой массив */
            var DOMList = DOM.body.childNodes;
            /** Далее выводим его */
            return DOMList;
         } else {
            /** Если тип — не строка, то вернём пустой массив */
            return [];
         };
      };

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

    var parseHTML = function (data, context, keepScripts) {
       /** Если к нам отправили не строку, то вернуть пустой массив */
       if (typeof data !== 'string') {
          return [];
       }
       /**
        * Мы принимаем свойство context
        * как keepScripts, если в нём
        * указан true/false
        */
       if (typeof context === 'boolean') {
          keepScripts = context;
          context = false;
       };
      
       var DOM, DOMList;
    
       if (!context || context === document) {
          /** Создаём HTML страницу */
          DOM = document.implementation.createHTMLDocument() || new DOMParser().parseFromString('', 'text/html');
          /** Внесём в него HTML строку */
          DOM.body.innerHTML = data;
          /** Получаем наше пропарсенное содержимое */
          DOMList = DOM.body.childNodes;
        } else if (context.nodeType) {
          /** Создаём новый элемент, через который будем парсить код */
          DOM = document.createElement('div');
          /** Добавляем этот элемент в целевой элемент */
          context.appendChild(DOM);
          /** Добавляем содержимое в элемент */
          DOM.innerHTML = data;
          /** Получаем список элементов */
          DOMList = DOM.childNodes;
          /** Удаляем DOM */
          DOM.parentNode.removeChild(DOM);
       };
      
         /** Если нужно запустить скрипт, указанный в коде */
         if (keepScripts === true) {
          /** Получаем список скриптов */
          var scripts = DOM.scripts || DOM.querySelectorAll('script');
    
          /** Проверяем наличие файлов скрипта */
          if (scripts.length) {
             /** Перебираем все скрипты */
             for (var i = 0; i < scripts.length; i++) {
                /** Создаём новый элемент скрипта */
                var script = document.createElement('script');
                /** Передаём новосозданному элементу скрипта все параметры */
                script.innerHTML = scripts[i].innerHTML;
                if (scripts[i].attributes) script.attributes = scripts[i].attributes;
                /** Добавляем скрипт в HEAD для запуска */
                document.head.appendChild(script);
                /** Удаляем скрипт из HEAD */
                script.parentNode.removeChild(script);
             };
          };
       };
    
       /** Перебираем значения в обычный массив и выводим */
       return (function () {
          var array = [];
    
          for (var i = 0; i < DOMList.length; i++) {
             array[array.length] = DOMList[i];
          };
    
          return array;
       })();
    };

    Реализация и аналог функции объединения массивов — jQuery.merge()

    Если мы заглянем в исходники jQuery, то увидим, что у них данная функция реализуется следующим образом:

    var merge = function( first, second ) {
       var len = +second.length, /** Я не знаю зачем они преобразуют second.length в число, если по идее, это и так число (если кто-то знает, пишите в комментариях) */
          j = 0,
          i = first.length;
    
       /** Перебираем значения второго массива */
       for ( ; j < len; j++ ) {
          /**
           * Добавляем новое значение в первый массив
           * и за одно плюсует счётчик количества
           * элементов в первом массиве
           */
          first[ i++ ] = second[ j ];
       }
    
       /** Записываем новое количество элементов в массиве */
       first.length = i;
    
       /** Выводим готовый массив */
       return first;
    };

    Альтернатив у функции jQuery.merge() есть несколько:

    1. Первой альтернативой является функция Array.concat();, которая объединяет массивы.
      [0, 1, 2].concat([3, 4, 5]); // [0, 1, 2, 3, 4, 5]

      Но данная функция не подойдёт, если вы хотите присоединить к массиву HTML коллекцию или список Node, так как у вас выведется массив в массиве, вместо объединения.

      Но это довольно легко исправить: нужно преобраовать живой массив в обычный. Это можно сделать с помощью функции [].slice.call().

      [0, 1, 2].concat( [].slice.call(document.querySelectorAll(selector)) ); // [0, 1, 2, element, element]

      Или же это можно сделать перебрав все элементы живого массива и переместив их в обычный массив.

      var elements = document.querySelectorAll(selector);
      var array = [];
      for (var i = 0; i < elements.length; i++) {
         array[array.length] = elements[i];
      };
      [0, 1, 2].concat(array); // [0, 1, 2, element, element]

    2. Вторая альтернатива — перебрать элементы второго массива и перенести их в первый. Эту же технологию можно наблюдать в стандартной функции jQuery.
      var merge = function (array0, array1) {
         for (var i = 0; i < array1.length; i++) {
            array0[array0.length] = array1[i];
            /**
             * Или array0.push(array1[i]);
             * если первый массив не живой
             */
         };
      
         return array0;
      };
      merge([0, 1, 2], [3, 4, 5]); // [0, 1, 3, 4, 5, 6]

  2. Проверяем, является ли аргумент context объектом с параметрами. Если да, то записываем все параметры аргументами в DOM элементы.
    if ({}.toString.call(context) === '[object Object]') {
       for (var i = 0; i < array.length; i++) {
          for (var argument in context) {
             array.setAttribute(argument, context[argument]);
          };
       };
    };

  3. Выводим готовый массив с элементами

Если первый аргумент не HTML код, значит он является селектором. В jQuery элементы по селектору ищутся с помощью другой библиотеки именуемой Sizzle. Преимущество этой библиотеки в том, что её поиск по селектору начинает работать начиная с IE7+.

Поиск элементов по селектору, с помощью этой библиотеки, выглядит следующим образом:

Sizzle( String selector );

Но я воспользуюсь её JavaScript альтернативой — document.querySelectorAll( String selector );. Единственный недостаток этого метода перед библиотекой Sizzle — он начинает работать только с IE9+.

Прежде чем выводить элементы по селектору, нам нужно проверить аргумент context и убедится, что он не является элементом или элементами, в которых нужно искать элементы по селектору. Я не буду расписывать каждый шаг, так как в коде объяснил все шаги.

/** 
 * Проверяем, является ли аргумент context элементом или 
 * селектором элементов, в которых нужно искать элементы 
 * по селектору из аргумента selector
 */
/** Если аргумент context является элементом */
if (context && context.nodeType) {
   /** Получаем список элементов из родительского */
   var Elements = context.querySelectorAll(selector);
   /** Переносим список элементов в массив и выводим */
   return jQuery.merge(array, Elements);
 /** Если аргумент context является живым массив */
} else if ({}.toString.call(context) === '[object HTMLCollection]' || {}.toString.call(context) === '[object NodeList]') {
   /** 
    * Перебираем всех родителей и нужные
    * дочерние элементы переносим в массив
    */
   for (var i = 0; i < context.length; i++) {
      /** Получаем список элементов из родительского */
      var Elements = context[i].querySelectorAll(selector);
      /** Переносим список элементов в массив */
      jQuery.merge(array, Elements);
   };
   /** Выводим массив */
   return array;
/** Если аргумент context является селектором */
} else if (typeof context === 'string') {
   /** Получаем все родительские элементы
    * по селектору
    */
   var Parents = document.querySelectorAll(context);
   /** 
    * Перебираем всех родителей и нужные
    * дочерние элементы переносим в массив
    */
   for (var i = 0; i < Parents.length; i++) {
      /** Получаем список элементов из родительского */
      var Elements = Parents[i].querySelectorAll(selector);
      /** Переносим список элементов в массив */
      jQuery.merge(array, Elements);
   };
   /** Выводим массив */
   return array;
/** 
 * Если ни одни из вариантов не подходит, 
 * то осуществляем обычный поисх элементов
 */
} else {
   /** Получаем список элементов по селектору */
   var Elements = document.querySelectorAll(selector);
   /** Выводим массив */
   return jQuery.merge(array, Elements);
};

В jQuery все эти проверки заменены функцией поиска — .find();. Поэтому запись jQuery(selector, elements); является сокращённой функцией jQuery(elements).find(selector);

Сейчас наша основная функция выглядит следующим образом:

var init = jQuery.prototype.init = function (selector, context) {
   /** Для начала создаём массив, в который будем скидывать нужные элементы */
   var array = [];
   /** Сначала мы проработаем вариан,
    * когда кодер указал селектор 
    * или HTML код в первом аргументе
    */
   if (typeof selector === 'string') {
      /** Проверяем, не является ли селектор HTML кодом */
      if (selector[0] === "<" && selector[selector.length - 1] === ">" && selector.length >= 3) {
         /** Парсим HTML код и объединям с основным массивом */
         jQuery.merge(array, jQuery.parseHTML(selector, context && context.nodeType ? context.ownerDocument || context : document, true));
         /** Проверяем, является ли аргумент context объектом 
          * с параметрами. Если да, то записываем все параметры 
          * аргументами в DOM элементы. 
          */
         if ({}.toString.call(context) === '[object Object]') {
            for (var i = 0; i < array.length; i++) {
               for (var argument in context) {
                  array.setAttribute(argument, context[argument]);
               };
            };
         };
         /** Выводим массив */
         return array;
      /** Если это не HTML, то обычный селектор */
      } else {
         /** 
          * Проверяем, является ли аргумент context элементом или 
          * селектором элементов, в которых нужно искать элементы 
          * по селектору из аргумента selector
          */
         /** Если аргумент context является элементом */
         if (context && context.nodeType) {
            /** Получаем список элементов из родительского */
            var Elements = context.querySelectorAll(selector);
            /** Переносим список элементов в массив и выводим */
            return jQuery.merge(array, Elements);
         /** Если аргумент context является живым массив */
         } else if ({}.toString.call(context) === '[object HTMLCollection]' || {}.toString.call(context) === '[object NodeList]') {
            /** 
             * Перебираем всех родителей и нужные
             * дочерние элементы переносим в массив
             */
            for (var i = 0; i < context.length; i++) {
               /** Получаем список элементов из родительского */
               var Elements = context[i].querySelectorAll(selector);
               /** Переносим список элементов в массив */
               jQuery.merge(array, Elements);
            };
            /** Выводим массив */
            return array;
         /** Если аргумент context является селектором */
         } else if (typeof context === 'string') {
            /** Получаем все родительские элементы
             * по селектору
             */
            var Parents = document.querySelectorAll(context);
            /** 
             * Перебираем всех родителей и нужные
             * дочерние элементы переносим в массив
             */
            for (var i = 0; i < Parents.length; i++) {
               /** Получаем список элементов из родительского */
               var Elements = Parents[i].querySelectorAll(selector);
               /** Переносим список элементов в массив */
               jQuery.merge(array, Elements);
            };
            /** Выводим массив */
            return array;
         /** 
          * Если ни одни из вариантов не подходит, 
          * то осуществляем обычный поисх элементов
          */
         } else {
            /** Получаем список элементов по селектору */
            var Elements = document.querySelectorAll(selector);
            /** Выводим массив */
            return jQuery.merge(array, Elements);
         }
      };
   /** Далее проверяем, не является ли первый аргумент DOM элементом */
   } else if (selector.nodeType) {
   /** 
    * Следующая проверка будет на ситуацию
    * если первый аргумент является массивом
   */
   } else if ({}.toString.call(selector) === '[object Array]') {
   /**
    * Следующей проверкой будет вариант,
    * когда первый аргумент является объектом
    */
   } else if ({}.toString.call(selector) === '[object Object]') {
   /** Теперь проверяем, может это живой массив элементов? */
   } else if ({}.toString.call(selector) === '[object HTMLCollection]' || {}.toString.call(selector) === '[object NodeList]') {
   /** Если первый аргумент — это функция */
   } else if ({}.toString.call(selector) === '[object Function]') {
   /** Если же ни однин тип не подошёл, то выводим пустой массив */
   } else {
      return array;
   }
};

Теперь рассмотрим второе условие — если селектор является DOM элементом. Если селектор является DOM элементом, то мы просто вставляем этот элемент в основной массив, который потом выводим.

/** Вставляем элемент в массив */
array[0] = selector;
/** Выводим основной массив */
return array;

Перейдём к третьему условию — если селектор является массивом. Если если это массив, то мы объединяем его с основным массивом, который потом выводим. Объединять массивы мы будем с помощью вышенаписанной функции.

/** Объединям массивы */
jQuery.merge(array, selector);
/** Выводим основной массив */
return array;

Теперь о четвёртом условии — если селектор является объектом с параметрами. Если селектор является объектом с параметрами, то как и в варианте с DOM элементом, просто вносим объект в массив и выводим.

/** Вставляем объект в массив */
array[0] = selector;
/** Выводим основной массив */
return array;

Рассмотрим пятое условия — если селектор является живым массивом. Если селектор является живым массивом, то мы переносим элементы с него в основной массив. Действия выглядят, как и в случае с массивом.

/** Переносим элемент из живого массива в основной массив */
jQuery.merge(array, selector);
/** Выводим основной массив */
return array;

Осталось рассмотреть последнее условие — если селектор является функцией. Когда селектор — функцией, то функция jQuery( Function ); является сокращённой функцией jQuery(document).ready( Function );.

Реализация и аналог функции запуска кода после загрузки DOM — jQuery(document).ready( Function );

Когда мы посмотрим в исходники 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,
      // Счетчик, чтобы отслеживать количество элементов, ожидающих до начала события.
      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)) {
          // Проверяем, что body существует, в случае, если IE наложает.
          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) {
                // Проверяем, что имеются аргументы
                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() {
      // Проверяем, что body существует, в случае, если 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;
})();

Если мы рассмотрим код, то увидим, что он построен на двух отдельных вариантах запуска скрипта после загрузки DOM:

  1. Первый вариант — запуск скрипта через событие DOMContentLoaded повешенного на document. Этот вариант будет работать начиная с IE9+.
    document.addEventListener('DOMContentLoaded', function() {
       // Ваш скрипт
    }, false);

  2. Второй вариант — с помощью события readystatechange повешенного на document и проверкой readyState. Этот вариант более старый, поэтому он будет работать начиная с IE4+.
    document.onreadystatechange = function(){
       if(document.readyState === 'complete'){
          // Ваш скрипт
       }
    };

Есть и другие способы запуска скрипта после загрузки DOM:

  1. Один из простых способов ― перенести <script></script> в body после всех элементов. При таком расположении сначала загрузится DOM, а потом скрипт.
    <body>
       <div>...</div>
       <script>
          // Ваш скрипт
       </script>
    </body>

  2. Этот вариант немного странный. Можно создать особую функцию и вызывать её через скрипт в конце body.
    function onload() {
       // Ваш скрипт
    };

    <body>
       ...
       <script>
          onload();
       </script>
    </body>

  3. Один из популярных вариантов — установить для body событие onload.
    function myFunc() {
       // Ваш скрипт
    };

    <body onload="myFunc()">...</body>

Эту функцию я заменю одной из альтернатив — событием readystatechange, о которой писал выше.

/** Запускаем функции после загрузки DOM дерева */
document.onreadystatechange = function(){
   if(document.readyState === 'complete'){
      selector();
   };
};
/** Вернём эту же функцию */
return selector;

Всё, мы записали действия для всех условий и наша функция практически готова. Осталось к параметру __proto__ основного массива присвоить прототип главной функции, чтобы потом можно было создавать дополнительный функционал для jQuery объектов. Объект __proto__ довольно новый и начинает работать только с IE11+. Поэтому мы сделаем некий полфилл.

if (array.__proto__) {
   array.__proto__ = jQuery.prototype;
} else {
   for (var param in jQuery.prototype) {
      array[param] = jQuery.prototype[param];
   };
};

Функция готово. В конечном итоге она будет выглядеть следующим образом:

/** Создаём функцию — обложку */
var jQuery = function (selector, context) {
   return new jQuery.prototype.init(selector, context);
};

/** Создаём для функции прототип */
jQuery.prototype = {
   constructor: jQuery
};

/** Создаём основную функцию */
var init = jQuery.prototype.init = function (selector, context) {
   /** Для начала создаём массив, в который будем скидывать нужные элементы */
   var array = [];
   /** 
    * К его proto массива присваиваем
    * прототип главной функции,
    * чтобы потом можно было 
    * создавать дополнительный
    * функционал для jQuery объектов
   */
   /** Объект __proto__ достаточно новый
    * Начинает работать начиная с IE11+
    * Поэтому мы создадим некий полифилл
    */
   if (array.__proto__) {
      array.__proto__ = jQuery.prototype;
   } else {
      for (var param in jQuery.prototype) {
         array[param] = jQuery.prototype[param];
      };
   };
   /** Сначала мы проработаем вариан,
    * когда кодер указал селектор 
    * или HTML код в первом аргументе
    */
   if (typeof selector === 'string') {
      /** Проверяем, не является ли селектор HTML кодом */
      if (selector[0] === "<" && selector[selector.length - 1] === ">" && selector.length >= 3) {
         /** Парсим HTML код и объединям с основным массивом */
         jQuery.merge(array, jQuery.parseHTML(selector, context && context.nodeType ? context.ownerDocument || context : document, true));
         /** Проверяем, является ли аргумент context объектом 
          * с параметрами. Если да, то записываем все параметры 
          * аргументами в DOM элементы. 
          */
         if ({}.toString.call(context) === '[object Object]') {
            for (var i = 0; i < array.length; i++) {
               for (var argument in context) {
                  array.setAttribute(argument, context[argument]);
               };
            };
         };
         /** Выводим массив */
         return array;
         /** Если это не HTML, то обычный селектор */
      } else {
         /** 
          * Проверяем, является ли аргумент context элементом или 
          * селектором элементов, в которых нужно искать элементы 
          * по селектору из аргумента selector
          */
         /** Если аргумент context является элементом */
         if (context && context.nodeType) {
            /** Получаем список элементов из родительского */
            var Elements = context.querySelectorAll(selector);
            /** Переносим список элементов в массив и выводим */
            return jQuery.merge(array, Elements);
            /** Если аргумент context является живым массив */
         } else if ({}.toString.call(context) === '[object HTMLCollection]' || {}.toString.call(context) === '[object NodeList]') {
            /** 
             * Перебираем всех родителей и нужные
             * дочерние элементы переносим в массив
             */
            for (var i = 0; i < context.length; i++) {
               /** Получаем список элементов из родительского */
               var Elements = context[i].querySelectorAll(selector);
               /** Переносим список элементов в массив */
               jQuery.merge(array, Elements);
            };
            /** Выводим массив */
            return array;
            /** Если аргумент context является селектором */
         } else if (typeof context === 'string') {
            /** Получаем все родительские элементы
             * по селектору
             */
            var Parents = document.querySelectorAll(context);
            /** 
             * Перебираем всех родителей и нужные
             * дочерние элементы переносим в массив
             */
            for (var i = 0; i < Parents.length; i++) {
               /** Получаем список элементов из родительского */
               var Elements = Parents[i].querySelectorAll(selector);
               /** Переносим список элементов в массив */
               jQuery.merge(array, Elements);
            };
            /** Выводим массив */
            return array;
            /** 
             * Если ни одни из вариантов не подходит, 
             * то осуществляем обычный поисх элементов
             */
         } else {
            /** Получаем список элементов по селектору */
            var Elements = document.querySelectorAll(selector);
            /** Выводим массив */
            return jQuery.merge(array, Elements);
         }
      };
      /** Далее проверяем, не является ли первый аргумент DOM элементом */
   } else if (selector.nodeType) {
      /** Вставляем элемент в массив */
      array[0] = selector;
      /** Выводим основной массив */
      return array;
      /** 
       * Следующая проверка будет на ситуацию
       * если первый аргумент является массивом
      */
   } else if ({}.toString.call(selector) === '[object Array]') {
      /** Объединям массивы */
      jQuery.merge(array, selector);
      /** Выводим основной массив */
      return array;
      /**
       * Следующей проверкой будет вариант,
       * когда первый аргумент является объектом
       */
   } else if ({}.toString.call(selector) === '[object Object]') {
      /** Вставляем объект в массив */
      array[0] = selector;
      /** Выводим основной массив */
      return array;
      /** Теперь проверяем, может это живой массив элементов? */
   } else if ({}.toString.call(selector) === '[object HTMLCollection]' || {}.toString.call(selector) === '[object NodeList]') {
      /** Переносим элемент из живого массива в основной массив */
      jQuery.merge(array, selector);
      /** Выводим основной массив */
      return array;
      /** Если первый аргумент — это функция */
   } else if ({}.toString.call(selector) === '[object Function]') {
      /** Запускаем функции после загрузки DOM дерева */
      document.onreadystatechange = function(){
         if(document.readyState === 'complete'){
            selector();
         };
      };
      /** Вернём эту же функцию */
      return selector;
      /** Если же ни однин тип не подошёл, то выводим пустой массив */
   } else {
      return array;
   }
};

jQuery.merge = function (first, second) {
   var len = +second.length, /** Я не знаю зачем они преобразуют second.length в число, если по идее, это и так число */
      j = 0,
      i = first.length;

   /** Перебираем значения второго массива */
   for (; j < len; j++) {
      /**
       * Добавляем новое значение в первый массив
       * и за одно плюсует счётчик количества
       * элементов в первом массиве
       */
      first[i++] = second[j];
   }

   /** Записываем новое количество элементов в массиве */
   first.length = i;

   /** Выводим готовый массив */
   return first;
};

jQuery.parseHTML = function (data, context, keepScripts) {
   /** Если к нам отправили не строку, то вернуть пустой массив */
   if (typeof data !== 'string') {
      return [];
   }
   /**
    * Мы принимаем свойство context
    * как keepScripts, если в нём
    * указан true/false
    */
   if (typeof context === 'boolean') {
      keepScripts = context;
      context = false;
   };

   var DOM, DOMList;

   if (!context || context === document) {
      /** Создаём HTML страницу */
      DOM = document.implementation.createHTMLDocument() || new DOMParser().parseFromString('', 'text/html');
      /** Внесём в него HTML строку */
      DOM.body.innerHTML = data;
      /** Получаем наше пропарсенное содержимое */
      DOMList = DOM.body.childNodes;
   } else if (context.nodeType) {
      /** Создаём новый элемент, через который будем парсить код */
      DOM = document.createElement('div');
      /** Добавляем этот элемент в целевой элемент */
      context.appendChild(DOM);
      /** Добавляем содержимое в элемент */
      DOM.innerHTML = data;
      /** Получаем список элементов */
      DOMList = DOM.childNodes;
      /** Удаляем DOM */
      DOM.parentNode.removeChild(DOM);
   };

   /** Если нужно запустить скрипт, указанный в коде */
   if (keepScripts === true) {
      /** Получаем список скриптов */
      var scripts = DOM.scripts || DOM.querySelectorAll('script');

      /** Проверяем наличие файлов скрипта */
      if (scripts.length > 0) {
         /** Перебираем все скрипты */
         for (var i = 0; i < scripts.length; i++) {
            /** Создаём новый элемент скрипта */
            var script = document.createElement('script');
            /** Передаём новосозданному элементу скрипта все параметры */
            script.innerHTML = scripts[i].innerHTML;
            if (scripts[i].attributes) script.attributes = scripts[i].attributes;
            /** Добавляем скрипт в HEAD для запуска */
            document.head.appendChild(script);
            /** Удаляем скрипт из HEAD */
            script.parentNode.removeChild(script);
         };
      };
   };

   /** Перебираем значения в обычный массив и выводим */
   return (function () {
      var array = [];

      for (var i = 0; i < DOMList.length; i++) {
         array[array.length] = DOMList[i];
      };

      return array;
   })();
};

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

jQuery.prototype.myFunction = function () {...};

На этом я закончу эту статью. Главное помнить: jQuery тоже написан на JavaScript :)

Автор: Yuri Spivak

Источник

Поделиться


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