rosecabbagedragon
rosecabbagedragon

Reputation: 172

How can one handle racing between two callbacks on the same event that can be triggered multiple times?

I have two callbacks that need to be put on the same change event on the same item. For reasons that are not worth going into, I need to have these callbacks in separate on, I cannot unify them under a single on call. So I need these on calls to stay separate, for example:

$('body').on('change', '#my-div', function1);
$('body').on('change', '#my-div', function2); 

Now, I have an AJAX call inside function1. I would like for function2 to always execute after the execution of function1 is done, including after an AJAX response has been received inside function1. I asked ChatGPT and it advised me to use $.Deferred():

let function1Deferred = $.Deferred();
function function1() {
    function1Deferred = $.Deferred();
    $.ajax({
        url: 'your-url', // Replace with your URL
        method: 'GET',
        success: function(data) {
            console.log("Function1 AJAX request completed");
            function1Deferred.resolve(); 
        },
        error: function() {
            function1Deferred.reject();
        }
    });
}

function function2() {
    function1Deferred.done(function() {
        console.log("Function2 is now executing after function1 has completed.");
    });
}

To my mind, though, this only solves the problem the first time the change event gets triggered, because there's no guarantee that function1 will run first, before function2 - and therefore there's no guarantee that, on the second and third etc. trigger of the change event, the line function1Deferred = $.Deferred(); inside function1 will run before function2 runs, so function2 might well be the first to run, and run to the end, on the subsequent triggers of the change event.

Am I missing something, is the code actually achieving what I want and I'm just missing something about how Deferred works under the hood? If yes, what am I missing? If not, how can I solve my problem and ensure function2 always runs after function1 on subsequent triggers of the event, not just on the first one?

I just want to emphasize again that I need the calls of the on function to stay separate, I cannot use a single on('change', '#my-div', newFunction) call, this is not a solution for my problem in its larger context.

Upvotes: 0

Views: 64

Answers (2)

edi kemput
edi kemput

Reputation: 77

var formAjax = (formElement,beforeSubmit)=>{
    var dfrd = $.Deferred();
    $(formElement).ajaxSubmit({
        beforeSubmit,
        success:dfrd.resolve,
        error:dfrd.rejected
    });
    return dfrd.promise();
};
<form id="frmPost" method="post" action="path-to-submit">
<input type="text" id="isian">
<button>Submit</button>
</form>
<div id="hasil"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.form.min.js"></script>
<script>
$(function(){
  $('#frmPost').submit(function(ev){
    ev.preventDefault();
    formAjax('#frmPost',function(){
      if( $('#isian').val()=='' ){
        alert('empty');
        $('#isian').focus();
        return false;
      }else return true;
    }).then(()=>{
      $('#hasil').html('success submit');
    }).fail(err=>{
      $('#hasil').html(err.message);
    });
    return false;
  });
});
</script>

me using jquery.form
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.form.min.js"></script>

var formAjax = (formElement,beforeSubmit)=>{
    var dfrd = $.Deferred();
    $(formElement).ajaxSubmit({
        beforeSubmit,
        success:dfrd.resolve,
        error:dfrd.rejected
    });
    return dfrd.promise();
};

Upvotes: 0

Richard Deeming
Richard Deeming

Reputation: 31238

As presented, the code will not necessarily do what you want. If function2 triggers first:

  • The first time, it will reference the initial deferred object, which will never be completed, so function 2 will never run;
  • On subsequent runs, it will reference the deferred object from the previous run, which is already completed, so it will run before function 1.

Here's a demo which explicitly sets the deferred object to null to make it more obvious. If you click the "run F2+F1" button, you'll get a "function1Deferred is null" error.

const output = document.getElementById("output");

let function1Deferred = null;

function function1() {
    output.append("Running function 1\n");
    function1Deferred = $.Deferred();
    setTimeout(() => {
        output.append("Function 1 complete\n");
        function1Deferred.resolve();
    }, 1000);
}

function function2() {
    function1Deferred.done(() => {
        output.append("Running function 2\n");
    });
}

document.getElementById("run1").addEventListener("click", () => {
    try {
        function1Deferred = null;
        function1();
        function2();
    } catch (e) {
        output.append(`Error: ${e}\n`);
    }
});

document.getElementById("run2").addEventListener("click", () => {
    try {
        function1Deferred = null;
        function2();
        function1();
    } catch (e) {
        output.append(`Error: ${e}\n`);
    }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<pre id="output"></pre>
<p>
  <button id="run1" type="button">Run F1+F2</button>
  <button id="run2" type="button">Run F2+F1</button>
</p>

Given the constraints, the simplest option is probably to wrap the function2 body in a setTimeout call:

setTimeout(() => function1Deferred.done(() => {
    output.append("Running function 2\n");
}), 0);

This will return control to the calling function, allowing the other handlers to execute, before registering the done callback on the deferred object.

Updated demo

const output = document.getElementById("output");

let function1Deferred = null;

function function1() {
    output.append("Running function 1\n");
    function1Deferred = $.Deferred();
    setTimeout(() => {
        output.append("Function 1 complete\n");
        function1Deferred.resolve();
    }, 1000);
}

function function2() {
    setTimeout(() => function1Deferred.done(() => {
        output.append("Running function 2\n");
    }), 0);
}

document.getElementById("run1").addEventListener("click", () => {
    try {
        function1Deferred = null;
        function1();
        function2();
    } catch (e) {
        output.append(`Error: ${e}\n`);
    }
});

document.getElementById("run2").addEventListener("click", () => {
    try {
        function1Deferred = null;
        function2();
        function1();
    } catch (e) {
        output.append(`Error: ${e}\n`);
    }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<pre id="output"></pre>
<p>
  <button id="run1" type="button">Run F1+F2</button>
  <button id="run2" type="button">Run F2+F1</button>
</p>

Now, whichever button you click, you will not see the "Running function 2" message until after the "Function 1 complete" message.


Also, as @jabaa mentioned in the comments, $.Deferred is a precursor to the JavaScript Promise. Combined with async functions, you could simplify this code even more:

const output = document.getElementById("output");

/* https://stackoverflow.com/a/39538518/124386 */
const delay = (timeout, result = null) =>
  new Promise((resolve) => setTimeout(resolve, timeout, result));

let function1Deferred = null;

async function function1() {
  output.append("Running function 1\n");
  const { promise, resolve, reject } = Promise.withResolvers();
  function1Deferred = promise;

  await delay(1000);

  output.append("Function 1 complete\n");
  resolve();
}

async function function2() {
  await delay(0);
  await function1Deferred;
  output.append("Running function 2\n");
}

document.getElementById("run1").addEventListener("click", () => {
  try {
    function1Deferred = null;
    function1();
    function2();
  } catch (e) {
    output.append(`Error: ${e}\n`);
  }
});

document.getElementById("run2").addEventListener("click", () => {
  try {
    function1Deferred = null;
    function2();
    function1();
  } catch (e) {
    output.append(`Error: ${e}\n`);
  }
});
<pre id="output"></pre>
<p>
  <button id="run1" type="button">Run F1+F2</button>
  <button id="run2" type="button">Run F2+F1</button>
</p>

Upvotes: 1

Related Questions