Section critique en JavaScript ou jQuery

J'ai une page Web, dans laquelle un certain événement Ajax est déclenché de manière asynchrone. Cette section Ajax pourrait être appelée une fois ou plus d'une fois. Je n'ai pas le contrôle du nombre de fois où cet événement est déclenché, ni le moment.

En outre, il existe un certain code dans cette section Ajax qui devrait fonctionner comme une section critique, ce qui signifie, lorsqu'il est en cours d'exécution, aucune autre copie de ce code devrait être en cours d'exécution.

Voici un pseudo code:

  1. Exécutez le code JavaScript ou jQuery
  2. Entrez une section critique qui est Ajax (lorsqu'un certain processus attend un rappel de réponse, n'insérez pas cette section à nouveau, jusqu'à ce que ce processus soit terminé)
  3. Exécutez plus de code JavaScript ou jQuery

Ma question est: comment puis-je exécuter l'étape 2 de la façon décrite ci-dessus? Comment créer / garantir une section d'exclusion mutuelle en utilisant JavaScript ou jQuery.

Je comprends la théorie (sémaphores, verrous, … etc.), mais je ne pouvais pas mettre en œuvre une solution utilisant JavaScript ou jQuery.

MODIFIER

Dans le cas où vous proposez une variable booléenne pour entrer dans la section critique, cela ne fonctionnerait pas, et les lignes ci-dessous expliqueront pourquoi.

Le code pour une section critique serait le suivant (en utilisant les suggestions de variables booléennes):

load_data_from_database = function () { // Load data from the database. Only load data if we almost reach the end of the page if ( jQuery(window).scrollTop() >= jQuery(document).height() - jQuery(window).height() - 300) { // Enter critical section if (window.lock == false) { // Lock the critical section window.lock = true; // Make Ajax call jQuery.ajax({ type: 'post', dataType: 'json', url: path/to/script.php, data: { action: 'action_load_posts' }, success: function (response) { // First do some stuff when we get a response // Then we unlock the critical section window.lock = false; } }); // End of critical section } } }; // The jQuery ready function (start code here) jQuery(document).ready(function() { var window.lock = false; // This is a global lock variable jQuery(window).on('scroll', load_data_from_database); }); 

Maintenant, il s'agit du code de la section de verrouillage comme suggéré en utilisant une variable booléenne. Cela ne fonctionnerait pas comme indiqué ci-dessous:

  1. L'utilisateur se déplace vers le bas, (et en fonction de l'association jQuery(window).on('scroll', load_data_from_database); plus d'un événement scroll est déclenché.

  2. Supposons que deux événements de défilement soient déclenchés à peu près au même moment

  3. Les deux appellent la fonction load_data_from_database

  4. Le premier événement vérifie si window.lock est faux (la réponse est vraie, donc si l'instruction est correcte)

  5. Le deuxième événement vérifie si window.lock est faux (la réponse est vraie, donc si l'instruction est correcte)

  6. Le premier événement entre dans l'instruction if

  7. Le deuxième événement entre dans l'instruction if

  8. La première instruction définit window.lock sur true

  9. La deuxième instruction définit window.lock sur true

  10. La première instruction exécute la section critique Ajax

  11. La deuxième instruction exécute la section critique Ajax.

  12. Les deux terminent le code

Comme vous le remarquez, les deux événements sont déclenchés presque simultanément et les deux entrent dans la section critique. Un verrou n'est donc pas possible.

Je pense que l'information la plus utile que vous avez fournie ci-dessus était votre analyse du verrouillage.

  1. L'utilisateur se déplace vers le bas, (et en fonction de l'association jQuery(window).on('scroll', load_data_from_database); plus d'un événement scroll est déclenché.

  2. Supposons que deux événements de défilement soient déclenchés à peu près au même moment

  3. Les deux appellent la fonction load_data_from_database

  4. Le premier événement vérifie si window.lock est faux (la réponse est vraie, donc si l'instruction est correcte)

  5. Le deuxième événement vérifie si window.lock est faux (la réponse est vraie, donc si l'instruction est correcte)

Tout de suite, cela me dit que vous avez eu un malentendu commun (et assez intuitif).

Le Javascript est asynchrone, mais le code asynchrone n'est pas la même chose que le code concurrent. Dans la mesure où je comprends, "asynchrone" signifie que les sous-routines d'une fonction ne sont pas nécessairement explorées en profondeur d'abord, comme nous l'espérons en code synchrone. Certains appels de fonction (ceux que vous appelez "ajax") seront mis en attente et exécutés plus tard. Cela peut conduire à un code déroutant, mais rien n'est aussi dérangeant que de penser que votre code asynchrone fonctionne simultanément. "Concurrence" (comme vous le savez) est lorsque des instructions de différentes fonctions peuvent s'interfacer entre elles.

Les solutions comme les verrous et les sémaphores ne sont pas la bonne façon de penser au code asynchrone. Les promesses sont les bonnes. C'est ce qui rend la programmation sur Internet amusante et cool.

Je ne suis pas un gourou de la promesse, mais voici un violon de travail qui (je pense) démontre une solution.

 load_data_from_database = function () { // Load data from the database. Only load data if we almost reach the end of the page if ( jQuery(window).scrollTop() >= jQuery(document).height() - jQuery(window).height() - 300) { console.log(promise.state()); if (promise.state() !== "pending") { promise = jQuery.ajax({ type: 'post', url: '/echo/json/', data: { json: { name: "BOB" }, delay: Math.random() * 10 }, success: function (response) { console.log("DONE"); } }); } } }; var promise = new $.Deferred().resolve(); // The jQuery ready function (start code here) jQuery(document).ready(function() { jQuery(window).on('scroll', load_data_from_database); }); 

J'utilise une promesse globale pour m'assurer que la partie ajax de votre gestionnaire d'événements est appelée une seule fois. Si vous faites défiler vers le haut et vers le bas dans le violon, vous verrez que pendant que la demande ajax est en cours de traitement, de nouvelles demandes ne seront pas effectuées. Une fois la demande ajax terminée, de nouvelles demandes peuvent être effectuées à nouveau. Avec une chance, c'est le comportement que vous recherchiez.

Cependant, il existe des réserves assez importantes pour ma réponse: la mise en œuvre des promesses de jQuery est notoirement brisée. Ce n'est pas seulement quelque chose que les gens disent pour sonner intelligent, c'est en fait très important. Je suggère d'utiliser une bibliothèque de promesses différente et de la mélanger avec jQuery. Ceci est particulièrement important si vous commencez simplement à connaître les promesses.

EDIT : Sur une note personnelle, j'étais récemment dans le même bateau que vous. Il y a aussi peu que 3 mois, je pensais que certains gestionnaires d'événements que j'utilisais étaient entrelacés. J'ai été stupéfait et incrédule quand les gens ont commencé à me dire que javascript est simple. Ce qui m'a aidé, c'est comprendre ce qui se passe quand un événement est déclenché.

Dans le codage synchrone, nous sommes habitués à l'idée d'une "pile" de "cadres" représentant chacun le contexte d'une fonction. Dans javascript, et d'autres environnements de programmation asynchrones, la pile est complétée par une file d'attente. Lorsque vous déclenchez un événement dans votre code ou utilisez une demande asynchrone comme cet appel $.ajax , vous appuyez sur un événement sur cette file. L'événement sera traité la prochaine fois que la pile est claire. Par exemple, si vous avez ce code:

 function () { this.on("bob", function () { console.log("hello"); }) this.do_some_work(); this.trigger("bob"); this.do_more_work(); } 

Les deux fonctions font du do_some_work et do_more_work l'une après l'autre immédiatement. Ensuite, la fonction se terminera et l'événement que vous avez lancé commencera un nouvel appel de fonction (sur la pile) et "bonjour" apparaîtra dans la console. Les choses se compliquent si vous déclenchez un événement dans votre gestionnaire, ou si vous déclenchez un événement dans un sous-programme.

Tout va bien et bon, mais lorsque les choses commencent à être vraiment difficiles, c'est quand vous voulez gérer une exception. Le moment où vous entrez dans une terre asynchrone, vous laissez derrière vous le serment de «une fonction doit revenir ou lancer». Si vous êtes dans un gestionnaire d'événements, et vous lancez une exception, où sera-t-il pris? Ce,

 function () { try { $.get("stuff", function (data) { // uh, now call that other API $.get("more-stuff", function (data) { // hope that worked... }; }); } catch (e) { console.log("pardon me?"); } } 

Ne vous sauvera plus maintenant. Les promesses vous permettent de reprendre ce serment ancien et puissant en vous donnant un moyen de chaîner vos rappels ensemble et de contrôler où et quand ils reviennent. Donc, avec une bonne promesse d'API (pas jQuery), vous créez ces rappels d'une manière qui vous permet de faire des exceptions de la manière que vous attendez et de contrôler l'ordre d'exécution. Ceci, selon moi, est la beauté et la magie des promesses.

Quelqu'un m'arrête si je suis totalement désactivé.

Je recommanderais une file d'attente qui ne permet qu'un seul élément s'exécuter à la fois. Cela nécessitera une certaine modification (mais pas beaucoup) de votre fonction critique:

 function critical(arg1, arg2, completedCallback) { $.ajax({ .... success: function(){ // normal stuff here. .... // at the end, call the completed callback completedCallback(); } }); } var queue = []; function queueCriticalCalls(arg1, arg2) { // this could be done abstractly to create a decorator pattern queue.push([arg1, arg2, queueCompleteCallback]); // if there's only one in the queue, we need to start it if (queue.length === 1) { critical.apply(null, queue[0]); } // this is only called by the critical function when one completes function queueCompleteCallback() { // clean up the call that just completed queue.splice(0, 1); // if there are any calls waiting, start the next one if (queue.length !== 0) { critical.apply(null, queue[0]); } } } 

MISE À JOUR: solution alternative utilisant jQuery's Promise (requiert jQuery 1.8+)

 function critical(arg1, arg2) { return $.ajax({ .... }); } // initialize the queue with an already completed promise so the // first call will proceed immediately var queuedUpdates = $.when(true); function queueCritical(arg1, arg2) { // update the promise variable to the result of the new promise queuedUpdates = queuedUpdates.then(function() { // this returns the promise for the new AJAX call return critical(arg1, arg2); }); } 

Yup, la promesse d'un code plus propre a été réalisée. 🙂

Vous pouvez envelopper la section critique dans une fonction, puis échanger la fonction de manière à ne rien faire après la première exécution:

  // this function does nothing function noop() {}; function critical() { critical = noop; // swap the functions //do your thing } 

Inspiré par l'utilisateur @I Hate Lazy Function en javascript qui peut être appelé une seule fois