Me cuesta mucho pensar en las promesas de jQuery. He creado el siguiente fragmento para explorar las promesas de jQuery; informado por esta StackOverflow.

let q = [
  function () { setTimeout(function () { console.log("a - " + Date.now()); }, 5000); },
  function () { setTimeout(function () { console.log("b - " + Date.now()); }, 2500); },
  function () { setTimeout(function () { console.log("c - " + Date.now()); }, 0); }
];
    
function SerialCall(queue) {
  var d = $.Deferred().resolve();
   while (queue.length > 0) {
     d = d.then(queue.shift()); // you don't need the `.done`
   }
}

SerialCall(q);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

Tengo entendido que las promesas de jQuery deberían retrasar la ejecución de b y c mientras a se está ejecutando y, posteriormente, retrasar la ejecución de c mientras b se está ejecutando.

Esperaba que la salida fuera a, b, c pero obtengo c, b, a. Tenga en cuenta que elegí estos retrasos deliberadamente ('a': 5000, 'b': 2500, 'c': 0) para ilustrar el hecho de que las promesas de jQuery no bloquean la ejecución según lo planeado.

¿Qué me estoy perdiendo y cómo debo alterar el código para obtener el comportamiento esperado?

2
Frank 29 oct. 2017 a las 07:28

3 respuestas

La mejor respuesta

Cuando te equivocas es en pensar que una promesa bloquea la ejecución. No lo hace. Una promesa es solo otra forma de escribir una función de devolución de llamada; no agrega ninguna magia más allá de lo que JavaScript ofrece de forma nativa. (No estoy abordando async / await aquí, solo JavaScript "tradicional"). Todo su código se ejecuta hasta su finalización antes de que se llame a cualquier devolución de llamada.

Ahí es donde te encuentras con problemas. Su while bucle se ejecuta hasta su finalización antes de que se llame alguna de sus funciones de cola:

while (queue.length > 0) {
    d = d.then(queue.shift()); // you don't need the `.done`
}

Si desea que se invoquen varias funciones con tiempos de espera como lo está haciendo, la mejor manera de hacerlo es no disparar todas las llamadas setTimeout() en un solo trago como lo hace su código ahora. En su lugar, haga que cada una de las funciones callback comience la siguiente setTimeout(). De esta manera, solo tiene un tiempo de espera pendiente en cualquier momento, y cuando se activa, comienza el siguiente.

Escribí un complemento jQuery hace muchos años para esto llamado slowEach. (Realmente no depende de jQuery y la misma técnica también funcionaría para código que no sea jQuery).

El código no usa promesas (las precede varios años), pero el principio es el mismo si usa promesas o devoluciones de llamada tradicionales: comience con un solo setTimeout() y luego, cuando se llame a su función de devolución de llamada, inicie el siguiente setTimeout(). Esto secuencia los tiempos de espera de la manera en que probablemente esperaba que el simple ciclo while lo hiciera.

El código original slowEach() se puede encontrar en esta respuesta. Varias personas han realizado mejoras en el código desde entonces. En particular, aquí hay una versión que agrega una devolución de llamada onCompletion para que reciba una devolución de llamada diferente cuando toda la matriz ha sido procesado.

Este código no usa promesas, pero lo más importante, funciona . :-) Sería un ejercicio interesante adaptar el código para usar promesas.

Pero, de nuevo, no asuma que JavaScript esperará a que se haga una promesa antes de ejecutar la siguiente línea de código. A menos que esté utilizando async / await, cualquier ciclo ordinario que esté ejecutando - while, for, lo que sea - siempre se ejecutará hasta su finalización antes de cualquier de las devoluciones de llamada (devoluciones de llamada ordinarias o promesas) se ejecutan.

1
Michael Geary 29 oct. 2017 a las 06:56

ADVERTENCIA: esta respuesta solo aborda el I was expecting the output to be a, b, c but I'm getting c, b, a.. No resuelve el problema de la promesa.

En su código, lo que sea que espere (como se explica en Michael Geary arriba), 'a' sale después de 5 segundos, 'b' sale después de 2.5 segundos y 'c' sale inmediatamente.

Si desea que la 'a' salga antes que la 'c', su tiempo de espera (su tiempo de espera ) debe ser más corto.

let queue = [
    function () { 
      let waitingTime = 0 ;
      setTimeout(function () { console.log("a - " + Date.now()); }, waitingTime); },
    function () { 
      let waitingTime = 2500 ;
      setTimeout(function () { console.log("b - " + Date.now()); }, waitingTime); },
    function () { 
      let waitingTime = 5000 ;
      setTimeout(function () { console.log("c - " + Date.now()); }, waitingTime); }
];

function SerialCall(queue) {
    var d = $.Deferred().resolve();
    while (queue.length > 0) {
        d = d.then(queue.shift()); // you don't need the `.done`
    }
}

SerialCall(queue);
.as-console-wrapper{max-height:100%!important;top:0;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

Por cierto, cuanto más claro sea su código, más simple puede depurarlo, comprenderlo. Mira por ejemplo:

let queue = [
    function () { waitForMe('a', 0)    },
    function () { waitForMe('b', 2500) },
    function () { waitForMe('c', 5000) }
];

function SerialCall(queue) {
    var d = $.Deferred().resolve();
    while (queue.length > 0) {
        d = d.then(queue.shift()); // you don't need the `.done`
    }
}

function waitForMe(letter, someTime) {
  return setTimeout(function () { console.log(letter +" - " + Date.now()); }, someTime)
}

SerialCall(queue);
.as-console-wrapper{max-height:100%!important;top:0;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
2
29 oct. 2017 a las 07:13

Sugiero usar esta extensión jQuery para hacer una ejecución secuencial en una cola diferida.

Puede usar su lista de funciones de tiempo de espera como entrada de deferQueue y ejecutarlas dentro de una función principal (invocable).

$.fn.deferQueue = function(callable, options){
    options = Object(options);
		var it = (this.get() || [])[Symbol.iterator]();
		var stop = false, cond = null;
		var self = this;
		this.stop = function(){ stop=true; };
		this.end = function(_cond){ cond = _cond };
		var tid = 0;
		var iterate = function(){
			if(tid) clearTimeout(tid);
			var o = it.next();
			if(cond instanceof Function && cond(o.value)) return;
			if(o.done || stop) return;
			var d = callable.call(self, o.value);
			if(options.timeout) tid = setTimeout(function(){ d.reject('timeout'); }, options.timeout);
			if(options.success) d.done(options.success);
			if(options.fail) d.fail(options.fail);
			d[options.iterate || 'always'](iterate);
		}
		iterate();
		return this;
	}
  
  function log(text){
  	console.log('Log: '+text);
  }
  function error(text){
  	console.log('Error: '+text);
  }
  
  let q = [
  function (D) { setTimeout(function () { console.log("a - " + Date.now()); D.resolve('function 1'); }, 5000); },
  function (D) { setTimeout(function () { console.log("b - " + Date.now()); D.resolve('function 2') }, 2500); },
  function (D) { setTimeout(function () { console.log("c - " + Date.now()); D.resolve('function 3') }, 0); },
'https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png',
'https://unreachabe_domain/image.png',
'https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png',
null,
'https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png'
];
  
  $(function(){      	
    var
      display_timeout = parseInt($('#container').data('display-timeout')),
      loading_timeout = parseInt($('#container').data('loading-timeout'));
      
    $(q).deferQueue(function(e){
    	var D = $.Deferred();
      if(typeof(e) == 'string') $('<img/>').attr('src',e).on('load',function(){ setTimeout(function(){ D.resolve(e); },display_timeout)}) 
        .on('error', function(){ D.reject(e) })
      	.appendTo('#container');
      else if(e instanceof Function) e(D);
      D.done(log).fail(error);
      return D;
    },{iterate:'always',timeout:loading_timeout}).end(function(e){ return e===null; });
  
  })
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<pre>
  usage : $(&lt;iterable&gt;).deferQueue(&lt;function(element)&gt;,&lt;options&gt;)
  
  options:
      timeout: int(seconds)
      iterate: string(always | done | fail)
      success: function
      fail: function
</pre>
<div id="container" data-display-timeout="1000" data-loading-timeout="3000">

</div>
0
Chouettou 7 feb. 2018 a las 10:35