ghusse
ghusse

Reputation: 3210

Chained jQuery deferred, all progress callbacks are called even after a then statement

I'm trying to chain some calls based on jquery deferred object. To make it simple, I want to :

  1. Call an asynchronous method returning a deferred object
  2. Observe its progress
  3. When done, call another method
  4. Observe its progress

At first, I wrote something like that :

myFirstFunction()
  .progress(myFirstProgressCallback)
  .done(myFirstDoneCallback)
  .then(mySecondFunction)
  .progress(mySecondProgressCallback)
  .done(mySecondDoneCallback);

But I observed something I did not expected (after reading the docs, it seems to be the way it works) :

  1. myFirstDoneCallback is called only once myFirstFunction resolves its deferred object
  2. mySecondDoneCallback is also called only once
  3. but mySecondProgressCallback is called when myFirstFunction AND mySecondFunction calls notify on their own deferred object.

Example you can run it in this jsbin:

function async(number){
  var def = new $.Deferred();

  setTimeout(function(){
    def.notify("Hello from " + number);
  }, 300);

  setTimeout(function(){
    def.resolve("I'm done " +number);
  }, 600);

  return def.promise();
}

async(1)
  .progress(function(msg){
    console.log("First progress: " + msg);
  })
  .done(function(msg){
    console.log("First done: " +msg);
  })
  .then(function(){
    return async(2);
  })
  .progress(function(msg){
    console.log("Second progress: " + msg);
  })
  .done(function(msg){
    console.log("Second done: " +msg);
  });

Result in the console:

"First progress: Hello from 1"
"Second progress: Hello from 1"
"First done: I'm done 1"
"Second progress: Hello from 2"
"Second done: I'm done 2"

First reaction : "Why the hell ??????"

Second : "How can I do what I want ?"

I replaced my code by this one, which works great (jsbin):

function async(number){
  var def = new $.Deferred();

  setTimeout(function(){
    def.notify("Hello from " + number);
  }, 300);

  setTimeout(function(){
    def.resolve("I'm done " +number);
  }, 600);

  return def.promise();
}

async(1)
  .progress(function(msg){
    console.log("First progress: " + msg);
  })
  .done(function(msg){
  console.log("First done: " +msg);
})
.then(function(){
  return async(2)
    .progress(function(msg){
      console.log("Second progress: " + msg);
    })
    .done(function(msg){
      console.log("Second done: " +msg);
    });
  });

Output:

"First progress: Hello from 1"
"First done: I'm done 1"
"Second progress: Hello from 2"
"Second done: I'm done 2"

How to avoid registering the progress callback inside the function inside the "then" statement?

Upvotes: 2

Views: 453

Answers (1)

Gene C
Gene C

Reputation: 2030

Here is an idea that might work for you: Check the context inside the callback.

By default the context of a callback is the promise that fired the action:

var async2,
    async1 = async(1);

async1
    .done(function (msg) {
        if (this === async1) {
            console.log("First done: " + msg);
        }
    })
    .fail(function (msg) {
        if (this === async1) {
            console.log("First fail: " + msg);
        }
    })
    .progress(function (msg) {
        if (this === async1) {
            console.log("First progress: " + msg);
        }
    })
    .then(function (msg) {
        async2 = async(2);
        return async2;
    })
    .done(function (msg) {
        if (this === async2) {
            console.log("Second done: " + msg);
        }
    })
    .fail(function (msg) {
        if (this === async2) {
            console.log("Second fail: " + msg);
        }
    })
    .progress(function (msg) {
        if (this === async2) {
            console.log("Second progress: " + msg);
        }
    });

I'm not sure if this is a better solution than nesting the progress callback inside of the then. One issue is that the actions maybe executed with a specific context (using notifyWith, resolveWith, rejectWith).

More Info Than You Asked For

I discovered the same behavior a little while ago, felt the same frustration, and came to the same resolution you did. Since then I have done a little more research into how notify/progress works and this is what I have found:

then returns a new promise, but it also forwards all of the actions (resolve, reject, notify) from the former promise to the latter promise. In fact, as soon as you add error handling to your promise chain, you will see that this behavior extends to fail callbacks as well:

function async(number){
  var def = new $.Deferred();

  setTimeout(function(){
    def.notify("Hello from " + number);
  }, 300);

  setTimeout(function(){
    def.reject("I've failed " + number);
  }, 450);

  return def.promise();
}

async(1)
  .progress(function(msg){
    console.log("First progress: " + msg);
  })
  .fail(function(msg){
    console.log("First fail: " +msg);
  })
  .then(function(){
    return async(2);
  })
  .progress(function(msg){
    console.log("Second progress: " + msg);
  })
  .fail(function(msg){
    console.log("Second fail: " +msg);
  });

Output:

"First progress: Hello from 1"
"Second progress: Hello from 1"
"First fail: I've failed 1"
"Second fail: I've failed 1"

Even though the second async is never invoked, all of the progress and fail callbacks have been executed. The same thing, although seldom, will occur with the done handler if you provide it with anything but a function:

async(1)
  .progress(function(msg){
    console.log("First progress: " + msg);
  })
  .done(function(msg){
    console.log("First done: " +msg);
  })
  .then('foo')
  .progress(function(msg){
    console.log("Second progress: " + msg);
  })
  .done(function(msg){
    console.log("Second done: " +msg);
  });

Output:

"First progress: Hello from 1"
"Second progress: Hello from 1"
"First done: I'm done 1"
"Second done: I'm done 1"

So I guess what I am trying to say is that the behavior you are seeing with the progress callback is not inconsistent with how the Deferred object works.

On the onset of my investigation I was optimistic that we might be able to provoke the behavior we desire by using the Promises/A+ style: promise.then(doneFilter, failFilter, progressFilter):

async(1)
    .then(function (msg) {
        console.log("First done: " + msg);
        return async(2);
    },
    null /*Failure Handler*/,
    function (msg) {
        console.log("First progress: " + msg);
    })
    .then(function (msg) {
        console.log("Second done: " + msg);
    },
    null /*Failure Handler*/,
    function (msg) {
        console.log("Second progress: " + msg);
    });

Unfortunately, the results are no better:

"First progress: Hello from 1"
"Second progress: undefined"
"First done: I'm done 1"
"Second progress: Hello from 2"
"Second done: I'm done 2"

Interestingly the first execution of the second progress callback is not provided the correct value. I have not investigated that further except to confirm that Q (another implementation of promises that supports progress/notify) provides identical results.

Finally, I answered a question that helped me clarify why how this all works:

If all of the actions are forwarded to the next promise, why isn't the nested progress handler invoked by those forwarded actions?

The progress handler is added as a callback after the first promise is resolved and the next async task is pending. Unlike the done and fail, progress handlers need to be attached at the time that the corresponding action (notify) is taken.

Upvotes: 1

Related Questions