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.
As presented, the code will not necessarily do what you want. If function2
triggers first:
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.
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>