Éviter les multiples $(document).ready() dans vos pages web

Une chose qui me gène avec la $(document).ready() de la librairie jQuery, c'est que c'est une magnifique porte ouverte aux mauvaises pratiques JavaScript. Elle empèche les développeurs web en herbe de se pauser les bonnes questions et pire encore, comme j'ai pu le constater récemment, aux développeurs à priori chevronnés d'en faire de même...

Oui, le $(document).ready() peut être utilisé plus d'une fois dans un ensemble de fichier et oui, il peut être placé n'importe où dans une page HTML mais non, ce n'est absolument pas vous rendre service que de faire cela ! Ce n'est pas parce que l'on peut, que l'on doit.

Mettre ces déclarations un peu partout rend plus difficile la relecture et le debug du code en empêchant de savoir qui s'exécute avant qui sans regarder l'ordre d'appel des fichiers. Effectivement, si cela semble simple et pratique quand 3 fichiers se battent en duel, cela peut rapidement devenir complexe. De plus, si une exception est levé dans l'un des $(document).ready(), aucun des autres n’exécutera plus rien du tout. Pour finir, le code est ralenti lors de l'appel de plusieurs $(document).ready() contre un seul, ou contre aucun d'ailleurs.

$(document).ready() ZzzZz
Don’t let jQuery’s $(document).ready() slow you down

Dans cet article nous allons voir l'une des dizaines de façon de vous passer de $(document).ready() dans vos pages web. Le maître mot ? Un seul point d'entré pour l'ensemble du code exécuté sur une page. Vous trouverez également un exemple de la méthode décrite dans cet article dans ce repository GitHub.

Être composant orienté avec jQuery

JavaScript en vrac

Admettons que dans une page on trouve deux composants de Carrousel avec en pied de page un appel à la libraire jQuery, à l'extension jQuery OWL Carousel pour gérer des carrousels, et un fichier pour écrire notre code JavaScript.

<!-- Ici une image pleine, slidable -->
<div class="one-item-carousel">
    <div class="carousel">
        <div class="item"><img src="assets/fullimage1.jpg" alt="The Last of us"></div>
        <div class="item"><img src="assets/fullimage2.jpg" alt="GTA V"></div>
        <div class="item"><img src="assets/fullimage3.jpg" alt="Mirror Edge"></div>
    </div>
</div>

<!-- Ici plusieurs petites images qui défilent -->
<div class="multiple-item-carousel">
    <div class="carousel">
        <div class="item"><img src="assets/fullimage1.jpg" alt="The Last of us"></div>
        <div class="item"><img src="assets/fullimage2.jpg" alt="GTA V"></div>
        <div class="item"><img src="assets/fullimage3.jpg" alt="Mirror Edge"></div>
    </div>
</div>

<!-- ... -->

<!-- En pied de page, l'appel aux scripts -->
<script src="jquery.js"></script>
<script src="owl.carousel.js"></script>
<script src="common.js"></script>

Le fichier common.js pourrait alors ressembler à cela :

$(document).ready(function() {
    $(".one-item-carousel .carousel").owlCarousel({
        navigation : true,
        slideSpeed : 300,
        paginationSpeed : 400,
        singleItem:true 
    });
});

$(document).ready(function() {
    $(".multiple-item-carousel .carousel").owlCarousel({
        navigation : true,
        slideSpeed : 300,
        paginationSpeed : 400,
        singleItem:true 
    });
});

et une découpe en composant des fichiers pourrait être celle-ci :

component.one-item-carousel.js

$(document).ready(function() {
    $(".one-item-carousel .carousel").owlCarousel({
        navigation : true,
        slideSpeed : 300,
        paginationSpeed : 400,
        singleItem:true 
    });
});

component.multiple-item-carousel.js

$(document).ready(function() {
    $(".multiple-item-carousel .carousel").owlCarousel({
        navigation : true,
        slideSpeed : 300,
        paginationSpeed : 400,
        singleItem:true 
    });
});

Exécution en retour AJAX après que le document soit ready impossible

Que ce passerait t-il si .one-item-carousel était déjà dans le code source HTML de la page mais que .multiple-item-carousel était rapatrié par chargement AJAX ou Websocket ?

$(document).ready(function() {
    $.ajax("multiple-item-carousel.html", function() {
        // Rapatriement du code HTML de `.multiple-item-carousel` et injection dans la page.
    });
});

$(document).ready(function() {
    $(".one-item-carousel .carousel").owlCarousel({
        navigation : true,
        slideSpeed : 300,
        paginationSpeed : 400,
        singleItem:true 
    });
});

$(document).ready(function() {
    $(".multiple-item-carousel .carousel").owlCarousel({
        items : 10,
        itemsDesktop : [1000,5],
        itemsDesktopSmall : [900,3],
        itemsTablet: [600,2],
        itemsMobile : false
    });
});

Et bien .one-item-carousel .carousel serait bien exécuté mais pas .multiple-item-carousel .carousel car aucun élément ne serait ciblé quand le « document serait ready ». Et selon la rapidité de votre machine ou du réseau, peut-être que finalement ça passerait... Pire, votre seul moyen de ré-exécuter le code dans vos $(document).ready() est de supprimer et ré-injecter le script dans le HTML.

Appels de fonctions

« Mais c'est très simple ! Il suffit alors de déporter les appels dans des fonctions et d'en exécuter une en retour d'AJAX ! »

function loadOneItemCarousel() {
    $(".one-item-carousel .carousel").owlCarousel({
        navigation : true,
        slideSpeed : 300,
        paginationSpeed : 400,
        singleItem:true 
    });
}

function loadMultipleItemCarousel();() {
    $(".multiple-item-carousel .carousel").owlCarousel({
        items : 10,
        itemsDesktop : [1000,5],
        itemsDesktopSmall : [900,3],
        itemsTablet: [600,2],
        itemsMobile : false
    });
}

$(document).ready(function() {
    $.ajax("multiple-item-carousel.html", function() {
        // Rapatriement du code HTML de `.multiple-item-carousel` et injection dans la page.
        // Après rapatriement, on exécute le code JavaScript.
        loadMultipleItemCarousel();
    });
});

$(document).ready(function() {
    loadOneItemCarousel();
});

$(document).ready(function() {
    loadMultipleItemCarousel();
});

Inutilité de plusieurs $(document).ready()

On peut alors aisément transformer le code précédent en celui-ci et par la même occasion se débarrasser des multiples $(document).ready() :

function loadOneItemCarousel() {
    $(".one-item-carousel .carousel").owlCarousel({
        navigation : true,
        slideSpeed : 300,
        paginationSpeed : 400,
        singleItem:true 
    });
}

function loadMultipleItemCarousel() {
    $(".multiple-item-carousel .carousel").owlCarousel({
        items : 10,
        itemsDesktop : [1000,5],
        itemsDesktopSmall : [900,3],
        itemsTablet: [600,2],
        itemsMobile : false
    });
}

$(document).ready(function() {
    $.ajax("multiple-item-carousel.html", function() {
        // Rapatriement du code HTML de `.multiple-item-carousel` et injection dans la page.
        // Après rapatriement, on exécute le code JavaScript.
        loadMultipleItemCarousel(); 
    });

    loadOneItemCarousel();
    loadMultipleItemCarousel();
});

Namespace pour ses fonctions

C'est un bon début mais que va t'il se passer s'il y a des milliers de fonctions et de composants ?

function loadOneItemCarousel() {
    $(".one-item-carousel .carousel").owlCarousel({
        navigation : true,
        slideSpeed : 300,
        paginationSpeed : 400,
        singleItem:true 
    });
}

function playOneItemCarousel() {
    $(".one-item-carousel .carousel").trigger('owl.play', 1000);
}

function loadMultipleItemCarousel() {
    $(".multiple-item-carousel .carousel").owlCarousel({
        items : 10,
        itemsDesktop : [1000,5],
        itemsDesktopSmall : [900,3],
        itemsTablet: [600,2],
        itemsMobile : false
    });
}

function playMultipleItemCarousel() {
    $(".multiple-item-carousel .carousel").trigger('owl.play', 1000);
}

$(document).ready(function() {
    $.ajax("multiple-item-carousel.html", function() {
        // Rapatriement du code HTML de `.multiple-item-carousel` et injection dans la page.
        // Après rapatriement, on exécute le code JavaScript.
        loadMultipleItemCarousel(); 
    });

    loadOneItemCarousel();
    loadMultipleItemCarousel();
});

Vos nom de fonction vont se télescoper ou alors ils vont être très long. C'est le moment de les rassembler dans les mêmes objets.

var website = {},
    website.components = {};



// Composant one-item-carousel.
website.components["one-item-carousel"] = {};
website.components["one-item-carousel"].load = function () {
    $(".one-item-carousel .carousel").owlCarousel({
        navigation : true,
        slideSpeed : 300,
        paginationSpeed : 400,
        singleItem:true 
    });
};
website.components["one-item-carousel"].play = function () {
    $(".one-item-carousel .carousel").trigger('owl.play', 1000);
};
website.components["one-item-carousel"] = function () {
    website.components["one-item-carousel"].load();
    website.components["one-item-carousel"].play();
};



// Composant multiple-item-carousel.
website.components["multiple-item-carousel"] = {};
website.components["multiple-item-carousel"].load = function () {
    $(".multiple-item-carousel .carousel").owlCarousel({
        items : 10,
        itemsDesktop : [1000,5],
        itemsDesktopSmall : [900,3],
        itemsTablet: [600,2],
        itemsMobile : false
    });
}
};
website.components["multiple-item-carousel"].play = function () {
    $(".multiple-item-carousel .carousel").trigger('owl.play', 1000);
};
website.components["multiple-item-carousel"] = function () {
    website.components["multiple-item-carousel"].load();
    website.components["multiple-item-carousel"].play();
};



// Controller de page.
website.ajax = function () {
    $.ajax("multiple-item-carousel.html", function() {
        website.components["multiple-item-carousel"]();
    });
};
website = function () {
    // Lancement Controller.
    website.ajax();

    // Lancement Composant.
    website.components["one-item-carousel"]();
    website.components["multiple-item-carousel"](); 
};



// Le fameux Document Ready.
$(document).ready(function() {
    website.init();
});

Du code moins verbeux, du cloisonnement

On voit facilement qu'il y a redondance d'appel et que le code est toujours alimenter depuis le champ lexical global contrairement à un appel dans $(document).ready() qui à le mérite de cloisonner l'exécution de code. Allégeons tout ça, et créons de vrai cloisons sans $(document).ready().

var website = {};



// Composant one-item-carousel.
(function (publics) {
    var name = "one-item-carousel",
        privates = {};

    publics.components = publics.components || {};

    privates.load = function () {
        $("." + name + " .carousel").owlCarousel({
            navigation : true,
            slideSpeed : 300,
            paginationSpeed : 400,
            singleItem:true 
        });
    };
    privates.play = function () {
        $("." + name + " .carousel").trigger('owl.play', 1000);
    };
    privates = function () {
        privates.load();
        privates.play();
    };

    publics.components[name] = function () {
        privates();
    };
}(website));



// Composant multiple-item-carousel.
(function (publics) {
    var name = "multiple-item-carousel",
        privates = {};

    publics.components = publics.components || {};

    privates.load = function () {
        $("." + name + " .carousel").owlCarousel({
            items : 10,
            itemsDesktop : [1000,5],
            itemsDesktopSmall : [900,3],
            itemsTablet: [600,2],
            itemsMobile : false
        });
    };
    privates.play = function () {
        $("." + name + " .carousel").trigger('owl.play', 1000);
    };
    privates = function () {
        privates.load();
        privates.play();
    };

    publics.components[name] = function () {
        privates();
    };
}(website));



// Controller de page.
(function (publics) {
    var privates = {};

    privates.ajax = function () {
        $.ajax("multiple-item-carousel.html", function() {
            publics.components["multiple-item-carousel"]();
        });
    };
    publics.init = function () {
        privates.ajax();

        // Lancement Composant.
        publics.components["one-item-carousel"]();
        publics.components["multiple-item-carousel"]();
    };
}(website));



// Le fameux Document Ready.
$(document).ready(function() {
    website.init();
});

Note : Vous pourrez mieux comprendre cet exemple, notamment la différence entre publics et privates avec l'article Structurer le JavaScript de son site avec ou sans Framework

Composant orienté, sans $(document).ready()

Il est aisé à présent de découper notre code dans ces trois fichiers par exemple :

        <!-- ... -->

        <script src="component.one-item-carousel.js"></script>
        <script src="component.multiple-item-carousel.js"></script>
        <script src="common.js"></script>
    </body>
</html>

component.one-item-carousel.js

var website = website || {};

(function (publics) {
    var name = "one-item-carousel",
        privates = {};

    publics.components = publics.components || {};

    privates.load = function () {
        $("." + name + " .carousel").owlCarousel({
            navigation : true,
            slideSpeed : 300,
            paginationSpeed : 400,
            singleItem:true 
        });
    };
    privates.play = function () {
        $("." + name + " .carousel").trigger('owl.play', 1000);
    };
    privates = function () {
        privates.load();
        privates.play();
    };

    publics.components[name] = function () {
        privates();
    };
}(website));

component.multiple-item-carousel.js

var website = website || {};

(function (publics) {
    var name = "multiple-item-carousel",
        privates = {};

    publics.components = publics.components || {};

    privates.load = function () {
        $("." + name + " .carousel").owlCarousel({
            items : 10,
            itemsDesktop : [1000,5],
            itemsDesktopSmall : [900,3],
            itemsTablet: [600,2],
            itemsMobile : false
        });
    };
    privates.play = function () {
        $("." + name + " .carousel").trigger('owl.play', 1000);
    };
    privates = function () {
        privates.load();
        privates.play();
    };

    publics.components[name] = function () {
        privates();
    };
}(website));

Et de faire sauter le dernier $(document).ready() qui ne sert à rien puisque common.js est le dernier fichier appelé sur la page.

common.js

var website = website || {};

(function (publics) {
    var privates = {};

    privates.ajax = function () {
        $.ajax("multiple-item-carousel.html", function() {
            publics.components["multiple-item-carousel"]();
        });
    };
    publics.init = function () {
        privates.ajax();

        // Lancement Composant.
        publics.components["one-item-carousel"]();
        publics.components["multiple-item-carousel"]();
    };
}(website));



// Le fameux Document Ready disparait.
//$(document).ready(function() {
    website.init();
//});

Charger automatiquement les composants présent dans la page

C'est vrai que notre petit exercice nous permet de moduler l'appel des fonctions indépendamment de l'état du DOM mais elles ne se chargent plus automatiquement au chargement...

Ce petit ajustement de common.js répondra à cette problématique

var website = website || {};

(function (publics) {
    var privates = {};

    privates.ajax = function () {
        $.ajax("multiple-item-carousel.html", function() {
            publics.components["multiple-item-carousel"]();
        });
    };

    // Chargement automatique des composants présent dans la page.
    privates.loadComponents = function () {
        for (var i in publics.components) {
            // Sont t-il bien présent ?
            if ($("." + i).length > 0) {
                publics.components[i]();
            }
        }
    };

    publics.init = function () {
        privates.ajax();

        // Lancement Composant.
        privates.loadComponents();
    };
}(website));

website.init();

Déjà exécuté ?

Avec une petite classe, il est même possible de taguer les composants comme déjà exécutés pour ne pas les ré-exécuter une autre fois lors d'un rapatriement via AJAX ou Websocket.

Arrêter le cas par cas

Vous pouvez également en retour d'AJAX ou Websocket exécuté website.loadComponents() au lieu de website.components["multiple-item-carousel"]() par exemple (à condition d'accrocher loadComponents() à publics) pour ne plus vous préoccuper des composants renvoyé par les sources AJAX.

Exemple complet sur GitHub

Vous pouvez tester un exemple similaire à ce petit exercice dans le projet GitHub « ComponentAutoLoadTemplate ».