Безопасное использование .ready() перед подключением jQuery

document ready

Сегодня прочитал Перестаньте платить дань jQuery, хорошую статью от Sam Saffron, которая поясняет идею перемещения всех внешних скриптов в конец HTML документа, и метода который позволит использовать jQuery .ready() везде в вашем документе, даже если вы переместили сам jQuery в конец документа. Я метод немного доработал.

Его метод, по существу, это:

  1. В теге head включите скрипт который:
    • Определяет коллекцию,
    • Создает фальшивую функцию $ которая добавляет аргументы в коллекцию.
  2. В теле документа, прямо после того как вы подключили jQuery, добавте скрипт который:
    • Использует цикл из jQuery для прохождения по полекции,
    • ...и вызывает реальную функцию $ передавая ей аргумент.

Я решил поиграться немного с примерами которые дал Сем и понял что она работает только для одного варианта использования DOM ready

$(handler) // Где `handler` функция для вызова

Но jQuery также позволяет следующие форматы:

$(document).ready(handler)

$().ready(handler) // Хотя это и не рекомендуется

$(document).bind("ready", handler)

Решение

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

В теге head, добавте:

<script>(function(w,d,u){w.readyQ=[];w.bindReadyQ=[];function p(x,y){if(x=="ready"){w.bindReadyQ.push(y);}else{w.readyQ.push(x);}};var a={ready:p,bind:p};w.$=w.jQuery=function(f){if(f===d||f===u){return a}else{p(f)}}})(window,document)</script>

В теге body, прямо после подключения jQuery, добавте:

<script>(function($,d){$.each(readyQ,function(i,f){$(f)});$.each(bindReadyQ,function(i,f){$(d).bind("ready",f)})})(jQuery,document)</script>

Да, это нечитаемая смесь, но я раскрою детали (с красивыми именами переменных и функций).

После раскрытия скрипта из head, мы получим:

(function (w, d, u) {

    // Определение двоих коллекций для обработчиков
    w.readyQ = [];
    w.bindReadyQ = [];

    // добавлет обработчик в правильную коллекцию
    function pushToQ(x, y) {
        if (x == "ready") {
            w.bindReadyQ.push(y);
        } else {
            w.readyQ.push(x);
        }
    }

    // определение псевдоним для использования позже
    var alias = {
        ready: pushToQ,
        bind: pushToQ
    }

    // Определение фальшивой jQuery функции для вылова обработчиков 
    w.$ = w.jQuery = function (handler) {
        if (handler === d || handler === u) {
            // $(document).ready(handler), $().ready(handler)
            // и $(document).bind("ready", handler) возвращает псевдоним с методами pushToQ
            return alias;
        } else {
            // $(handler)
            pushToQ(handler);
        }
    }

})(window, document);

Если вы посмотрите на документацию метода jQuery .ready() то там объяснено как обработчики которые использовали .bind() на самом деле вызываются после всех остальных. Именно поэтому у нас две коллекции для обработчиков - для учета этого поведения.

После раскрытия скрипта с тега body (тот что сразу после jQuery), мы имеем:

(function ($, doc) {
    $.each(readyQ, function (index, handler) {
        $(handler);
    });
    $.each(bindReadyQ, function (index, handler) {
        $(doc).bind("ready", handler);
    });
})(jQuery, document);

Таким же путем как и в примерах Сема, мы используем метод jQuery .each() для правильного связывания наших обработчиков к DOM ready, но так как $(document).bind("ready", handler) должны быть вызваны раньше, мы связываем их тоже в правильном порядке

Example

Вот простой пример использования скриптов с выводом информации в консоль.

<!DOCTYPE html>
<html>
    <head>
        <title>Example</title>
        <script>(function(w,d,u){w.readyQ=[];w.bindReadyQ=[];function p(x,y){if(x=="ready"){w.bindReadyQ.push(y);}else{w.readyQ.push(x);}};var a={ready:p,bind:p};w.$=w.jQuery=function(f){if(f===d||f===u){return a}else{p(f)}}})(window,document)</script>
    </head>
    <body>
        <script>
            $(document).bind("ready", function () {
                console.log("Example D: $(document).bind(\"ready\", handler)");
            });
            $(document).ready(function () {
                console.log("Example A: $(document).ready(handler)");
            });
            $().ready(function () {
                console.log("Example B: $().ready(handler)");
            });
            $(function(){
                console.log("Example C: $(handler)");
            });
        </script>
        <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
        <script>(function($,d){$.each(readyQ,function(i,f){$(f)});$.each(bindReadyQ,function(i,f){$(d).bind("ready",f)})})(jQuery,document)</script>
    </body>
</html>

Результат работы:

Example A: $(document).ready(handler)
Example B: $().ready(handler)
Example C: $(handler)
Example D: $(document).bind("ready", handler)

Обратите внимание что хотя Example D первый в примере он использует $(document).bind("ready", handler) поэтому он поставлен в отдельную очередь, и исполнен после других трех примеров. Поведение точно такое как подразумевает jQuery.

Original