Ajax: async or not async? callbacks, promesses

Alors là tout de suite j’ai la flemme mais un codepen ou équivalent pourrait graaave aider à ce qu’on t’aide à debugger ton bidule.

Sinon j’essaierai de regarder demain mais là ce soir la flemme :sweat_smile:

Ah et sors les fonctions de tes fonctions.

tu ne peux pas travailler comme ça avec de l’asynchrone, il te faut utiliser soit un « callback » c’est à dire une fonction qui sera lancée dès que les données sont disponibles, soit un promise (je te le mets cette version pour la science mais peut être la version callback est plus simple).

Je te propose ça comme solution, à adapter/corriger pour tes besoin:

//CALLBACK
table.on('click', 'td.details-control', function () {

// A priori td et row fonctionnent puisque je vois bien la bonne valeur dans le console.log de ajaxFn ()
	var td = $(this),
	  row = table.row(td.closest('tr'));
	if (row.child.isShown()) {
	  row.child.hide();
	  td.html('⊕');
	} else {
                var callback = function(data){
                       row.child(data).show();
                       td.html('&CercleMinus;');
                }	
		chargement_ajax(row.data(),callback)
	} 
})

var chargement_ajax = function (d,callback) {
	
        // d[2] c'est le nb de lignes attendues pré-calculées
	if(d[2]>0) {
	
	   $.ajax(mon_url, {
		  async: true,
		  data: {MonParam: d[1]},
		  error: function (XMLHttpRequest, textStatus, errorThrown) {
			alert(errorThrown);
		  },
		}).done(function(data) {
			  if (typeof data !== 'undefined') {
				  result_ajax_json= data;
				  tableau_html=result_ajax_json.toString();

				  var result0 ='<b>éléments trouvée: </b> </br>'+
				  '<div class=\"TableauPlusHtml\">'+tableau_html+'</div>';
				  clearTimeout(timeOutId);
                                  callback(result0);
			  }
		});

	  }
}
1 « J'aime »

Une fois n’est pas coutume, je mets la version promise dans un autre post pour bien séparer.
J’ai essayé de commenter pour décortiquer le code mais en vrai c’est assez compliqué à appréhender au début, la version callback est largement suffisante pour toi.

//PROMISE
table.on('click', 'td.details-control', 
// on rajoute async ici, sans ça le mot clé await ne fonctionne pas, mais attention la fonction aussi devient asynchrone. Mais dans ce cas précis ça ne pose aucun problème, la fonction n'est pas utilisée ailleurs. :)
async function () {

// A priori td et row fonctionnent puisque je vois bien la bonne valeur dans le console.log de ajaxFn ()
	var td = $(this),
	  row = table.row(td.closest('tr'));
	if (row.child.isShown()) {
	  row.child.hide();
	  td.html('&oplus;');
	} else {
                try{
                    // chargement_ajax renvoit maintenant une promesse, grace au mot clé await, on sait qu'on veut attendre le résultat avant de passer à la suite. Avec du code asynchrone classique ça ne peut pas marcher mais grâce à cette façon de coder on retrouve un code quasi équivalent à du synchrone.
                     data = await chargement_ajax(row.data());
                     row.child(data).show();
                     td.html('&CercleMinus;');
                } catch(error){
                     alert(error);
                }
		
	} 
})

var chargement_ajax = function (d) {
        // On transforme l'appel ajax en promise, resolve et reject permettent d'indiquer à quels endroits la Promesse se résout en succès ou en erreur. Leur traitement est totalement géré en dehors de la promesse.
	return new Promise(function(resolve, reject){
        // d[2] c'est le nb de lignes attendues pré-calculées
	if(d[2]>0) {
	
	   $.ajax(mon_url, {
		  async: true,
		  data: {MonParam: d[1]},
		  error: function (XMLHttpRequest, textStatus, errorThrown) {
			// C'est ici qu'on a l'erreur.
                         reject(errorThrown);
		  },
		}).done(function(data) {
			  if (typeof data !== 'undefined') {
				  result_ajax_json= data;
				  tableau_html=result_ajax_json.toString();

				  var result0 ='<b>éléments trouvée: </b> </br>'+
				  '<div class=\"TableauPlusHtml\">'+tableau_html+'</div>';
				  clearTimeout(timeOutId);
                                  
                                   // C'est ici qu'on a le succès
                                   resolve(result0);
			  }
		});

	  }
          }
}

Merci :slight_smile: je teste cela demain.

Merci merci @Mistermick :partying_face: :sunglasses: Ça fonctionne très bien, même sur les réponses longues et en cliquant rapidement :slight_smile:

J’ai effectivement choisi la solution des callback plus conforme à mes besoins. Mais merci tout de même pour la solution avec des promesses.

1 « J'aime »

J’ai suivi une formation Angular sur internet, et j’avais découvert à ce moment là les promesses. (Note que je n’ai pas pratiqué sur Angular.)

Mais j’ignorais qu’en js « normal » hors framework les promesses sont utilisées.

Oui c’est assez récent, dans la vie de JS.

Et puis il y a autre chose : pour éviter les problèmes de compatibilité c’est plutôt déconseillé de retrouver ça dans les scripts javascript, en particulier les mots clé async et await qui sont encore plus récents.
Cependant quand on code avec des frameworks genre Angular, en réalité on est obligé de compiler nos sources parce que tel quel ça ne marcherait tout simplement pas sur nos navigateurs, c’est à dire changer le source Angular en javascript vanilla, html et css, au passage la moulinette va transformer tout le code javascript moderne en un équivalent plus compatible (tu peux voir comment ça marche sur https://babeljs.io/).

Autrement dit, quand on a tout l’outillage dont on a de toutes façon besoin quand on code avec un framework on peut se permettre d’utiliser les derniers gadgets javascript, mais si comme dans ton example ton code se retrouve utilisé directement il vaut mieux rester sur les classiques.

1 « J'aime »

Alors oui et non. En vrai la plupart des navigateurs qi ont 3 ou 4 ans supportent toutes les fonctions récentes du JS. Il y a une stratégie intéressante à ce sujet pour arrêter de bundler inutilement le JS en ES5 : https://philipwalton.com/articles/deploying-es2015-code-in-production-today/

J’ai beau lire 3 fois ton article @Thomasorus, je n’ai pas compris si babel (avec npm ou webpack) était nécessaire dans son explication. :slight_smile: J’ai l’impression que si. Et donc ce n’est pas pertinent pour mes 30 lignes en js.

Je m’en tiens donc à la conclusion de @Mistermick en tout cas dans ce cas précis de R Shiny en back qui nécessite pas ou peu de js en front, puisque c’est Shiny qui génére l’UI et les échanges de données avec celle-ci.

https://shiny.rstudio.com/

Édit : et un second lien :

https://mastering-shiny.org/basic-app.html

J’aurais dû être plus clair désolé : en gros l’article dit qu’il y a pas besoin de compiler ton JS si tu utilises les fonctionnalités ES2015 et que tes utilisateurs ont un navigateur de moins de 3 ans. Ils comprendront ton fichier.

Et dans le cas où tu devrais supporter de vieux navigateurs tout en utilisant des fonctionnalités ES2015 et en faisant attention aux performances, il vaut mieux faire deux bundles: un avec compilé en ES5 et un autre pas compilé.

1 « J'aime »

Merci maintenant pour ta version promesse de la réponse !

Je suis revenu à ton explication (*) pour des promesses imbriquées que je n’arrivais pas à résoudre mais pour VueJs 3 donc en ES6 a priori.

Donc je viens de résoudre un souci que je viens d’avoir, 10 mois après mon premier POC en VueJs, 5 mois après le début de mon vrai gros projet en VueJs, mais je m’aperçois qu’à part les copier-coller des appels des promesses dans des API axios directement ou via Pinia qui appelle ces API, à part ça je n’utilise quasiment pas consciemment les promesses jusqu’à aujourd’hui.

Je fais appel aux setTimeout() pour 2 raisons:

Voilà ce que je fais finalement.

Une api

  getMonApiSelect() {
    return axios
      .post(API_URL + "mon_url/", ApiFct.headers_authHeader())
      .then((response) => {
        return response.data;
      })
      .catch(function (error) {
        console.log(error);
        return Promise.reject(error);
      });
  }

L’appel mon api :

  .getMonApiSelect(monid)
  .then(
	(datas) => {
	  courant = datas.data;

	  Bla1= [
		...Bla1.map((x) => Object.assign({}, x)),
		...courant ,
	  ];

	  return Promise.resolve(
		Object.values(courant ).map((x) => x.uneautrecolonneid)
	  );
	},
	(error) => {
	  return Promise.reject(error);
	}
  )
  .then((uneautrecolonneid) => {
	Bla2.push(
	  Object.assign(
		{},
		...uneautrecolonneid
	  )
	);
  });

Et je n’utilise setTimeout() que pour une variable booléenne reactive() globale sur laquelle est branché un affichage v-if. J’avais failli ne pas utiliser le 2e .then() mais utiliser de façon erronée un setTimeout() pour attendre uneautrecolonneid.

Mais j’aurais sûrement d’autres questions sur les promesses.

(*) Par hasard juste avant la clôture automatique des 2 ans par Discourse.

1 « J'aime »

Ah bah ça fait plaisir d’aider, même après tout ce temps :slight_smile:
Si tu veux un code un peu plus moderne, tu peux convertir ça avec la formule async/await comme ci-dessous, j’ai essayé de faire un code qui ressemble au tien.

Je trouve que c’est plus lisible avec les await une fois qu’on a l’habiture, ça ressemble presque à du code synchrone. :slight_smile:

const promise1 = new Promise((resolve, reject) => {
	resolve('Success!');
});

promise1
.then((value) => {
	console.log(value);
	return Promise.resolve(value+value.substr(-1));
},(err) => {
	console.log(err);
}).then((value2) => {
	console.log(value2); 
})

-----

const promise1 = new Promise((resolve, reject) => {
  resolve('Success!');
});

const f = async function(){
	try{
		const value = await promise1
		console.log(value);
		const value2 = value+value.substr(-1);
		console.log(value2);
	} catch(err) {
		console.log(err);
	}
}

f();

2 « J'aime »

:slight_smile: .

Ok je note ça :slight_smile: .

Tiens ça me fait penser, quand on est dans le then (ou le async () => { ... Try{) on peut récupérer la valeur et l’affecter ou au DOM ou à des variables réactives (ou de portée supérieure).

Mais est-ce qu’il y a un moyen de récupérer la variable par

const f = async function(){
    let value 2
	try{
		const value = await promise1
		value2 = value+value.substr(-1);
		//"success!!"
	} 
....
    return value2;
}


let toto = f(); //"success!!"

f() renvoie une valeur et non une promesse.
À part remonter la déclaration de value2 en dehors de f() et de lire après coup value2, j’ai l’impression que c’est impossible en dehors du périmètre asynchrone. Bon après c’est pas illogique.

La seule chose permise ce sont les promesses consécutives ? D’ailleurs en async () => { ... await, comment tu écris simplement l’équivalent des 2 then() se suivant ?

La fonction f est asynchrone donc effectivement elle ne peut renvoyer qu’une promesse. Pour récupérer la valeur il faut rester dans un contexte asynchrone

const f2 = async function(){
   let toto = await f();
}

L’équivalent de deux then se suivant c’est deux await, par exemple:

   axios.post()
        .then((response) => {return anotherPromise(response)})
        .then((finalData) => finalData.data)
        .catch((err) => doSomethingWith(err))

devient

const f = async function(){
   try{
      response = await axios.post()
      finalData = await anotherPromise(response)
      return finalData.data
   } catch(err) {
      doSomethingWith(err)
   }
}

Ca c’est pour les promesses consécutives, si tu veux lancer des promesses en parallèle tu peux te tourner vers Promise.all ou Promise.race par exemple. Par exemple Promise.all prend un tableau de promesses et se résout dès que toutes les promesses du tableau sont elles mêmes résolues.
Pratique si par exemple tu as plusieurs services à lancer et que tu veux changer le DOM dès que tu as les données de tous ces services.

1 « J'aime »

Tiens. J’avais complètement oublié ça. :ok_hand:

Attention au petit cas piégeux:

Si l’une des promesses de l’itérable échoue, Promise.all échoue immédiatement et utilise la raison de l’échec (que les autres promesses aient été résolues ou non).

2 « J'aime »

Ok je le note :slight_smile:

Par contre je n’ai pas l’impression que la forme await async soit plus lisible que la forme then(). En effet j’ai tendance de plus en plus à préférer la forme d’un flux, pipe par les points (comme les %>% de R ou les map().filter() de JS entre autres: la forme then() est plus conforme à cela.

Édit : Par contre avec les 2 await dans le même Try on peut confronter des données entre elles peut-être plus simplement.

Globalement on s’en sort très bien avec les then, c’est dans quelques cas que async/await est mieux.

Par exemple si tu a plusieurs then chainés et que tu veux réutiliser le résultat de ton premier promise tu es obligé de redonner ce résultat tout le long de la chaine, c’est le genre de problématiques qui ne se posent pas avec await.
Sinon effectivement les erreurs sont gérés comme des exceptions classiques, tu peux gérer les erreurs de promesse dans le même bloc de code où tu gères tes autres exceptions…

Tiens un cas pratique.

Donc j’ai fait 2 then() imbriqués, tout baigne.

Par contre j’ai un souci après une boucle for.

for (){
  
  axios.post()
           .then((datas) => { a=[...a,...datas]   return Promise.resolve(datas)})
           .then((id) => { b.push( etc.. id)})

}
setTimeout(function () {
  dédoublonnage(a) // fonctionne
  Tri(a) // fonctionne
  dédoublonnage(b) 
  Tri(b) 
  Copie ailleurs (a)
  Copie ailleurs (b)
}, 1000);

En fait tout ce qui a trait à a fonctionne, mais par la suite je me suis aperçu que c’était aléatoire pour b, mais comme lors de mes tests ça arrive une fois sur 5 quand je vais trop vite en cliquant je ne l’ai pas vu tout de suite.

Je soupçonne 2 soucis de pertes de données de b pour cause de timeout ou en fait de promesses non suivies.

  • à chaque boucle for
  • après la boucle for.

Bizarrement pour a, sans doute du à la construction de la donnée, je n’ai pas de souci si le setTimeout() est suffisant, tandis que b suite à un click n’est pas pris en compte une fois sur 5.

Édit:

Je suis en train de lire cela.

Visiblement il faut rendre la fonction for asynchrone et éventuellement récupérer dans un then le résultat si on a encore des trucs à faire.

Les promesses paraissent au premier abord de loin compliquées, mais devant un vrai cas pratique on se rend compte de leur avancées plutôt que de jouer avec des setTimeout() pas forcément pas fiables pour gérer l’asynchrone.

Bon je vais essayer de mettre ça en place. :slight_smile:

Ah oui clairement c’est un problème de fonction asynchrone pas terminée qui t’arrive, je dirais dans ton cas de faire un truc du genre

const f = async function(){
    const arrayUrl = [url1, url2, url3] // array of URL
    const arrayAxios = arrayURL.map(url => axios.post(url)) // array of Promise
    const a = await Promise.all(arrayAxios) // await all Promise are resolved
    const b = ...

    dédoublonnage(a) // fonctionne
    Tri(a) // fonctionne
    dédoublonnage(b) 
    Tri(b) 
    Copie ailleurs (a)
    Copie ailleurs (b)
}

1 « J'aime »