javascript - Why a code continues when the function doesnt return value yet. (in xmlhttprequest, calling php file.) Is it related to Sync/Async functions?

Came here again with lame questions as I am in process of learning/coding.

I would like to change a property of a disable value on button: During function performing its job, button should be disabled, once function finishes and return the values, button should be enabled again.

In function which creates a buttons I am calling update() function which loading php file via XMLHttpRequest. Then running the php code and return values on page. I want to have button disabled during this time. But everytime I call the function the button will not change. Or if changed it was so fast that I didnt even saw it.

here is a code:

    global_button = document.createElement("button");
//    let btn1 = document.createElement("button");
    global_button.innerHTML = "UPDATE";
    global_button.id = "update";
    global_button.disabled = false;
    document.body.appendChild(global_button);
    document.getElementsByClassName("two")[0].append(global_button);
    global_button.addEventListener("click", function () {
        console.log("After CLICKED");
        global_button.disabled = true;
        update();
        global_button.disabled = false;
        console.log("AFTER FUNCTION RETURN VALUES");

update function:

var xmlhttp;

function loadFile(file, func){
    xmlhttp = new XMLHttpRequest();
    xmlhttp.onreadystatechange = func;
    xmlhttp.open("GET", file, true);
    xmlhttp.send();
}

function update(){
    loadFile("update.php", function(){
        if (xmlhttp.readyState == 4 && xmlhttp.status == 200){
            document.getElementById("content").innerHTML = xmlhttp.responseText;
        }
    });
}

When I checked the console, it shows both console logs immediately: "After CLICKED" and "AFTER FUNCTION RETURN VALUES" messages. And couples seconds later, result of the function appear. But button wont change whatsoever.

I am suspecting the sync/async functions ? I read something for the .open method and vale true/false, but nothing changed if I switched from true to false. Also thinking if I should put it on the loop or something which will check the button clicked ? But I thought that listener would do the job.

Can anybody check and give me an advice ? or correct my thinking if it's wrong?

many thanks all of you. :)

Answer

Solution:

The problem is indeed due to the asynchronous nature of the send method of XMLHttpRequest - and therefore of your update, which calls it.

When you call update(), which itself calls this:

loadFile("update.php", function(){
    if (xmlhttp.readyState == 4 && xmlhttp.status == 200){
        document.getElementById("content").innerHTML = xmlhttp.responseText;
    }
});

all that happens is that you set up an XMLHttpRequest object and use its send method to send a request, telling it to call this function:

function(){
    if (xmlhttp.readyState == 4 && xmlhttp.status == 200){
        document.getElementById("content").innerHTML = xmlhttp.responseText;
    }
});

as a "callback" when the readyState changes. (And in particular, when the request is complete and a response received.) But calling update does not wait for that state change to happen and block your code from running - hence the next lines of code, which set the disabled state of the button to false and log to the console - are executed straight away. So the button gets disabled but then instantly un-disabled, and therefore you never see it disabled. (In fact the browser will never even "paint" the screen with a disabled button, since it doesn't get a chance to do this while your code is running, so even if you could in theory do a freeze-frame here you would never see a disabled button.)

To fix it, you have to work with the asynchronous code you're using. Anything you want to happen after the state change has to take place in the callback function you pass it. So you can simply fix your problem by changing the update definition to this:

function update(){
    loadFile("update.php", function(){
        if (xmlhttp.readyState == 4 && xmlhttp.status == 200){
            document.getElementById("content").innerHTML = xmlhttp.responseText;
            global_button.disabled = false;
            console.log("AFTER FUNCTION RETURN VALUES");   
        }
    });
}

and delete those two lines of code from the place you've currently got them, after the update call.

Although note that this will only work if global_button is in scope inside update, which it might not be depending on how your code is structured (it probably shouldn't be to be honest). And even if it does, it's not good to hardcode your update to always undisable the button afterwards, with no guarantee the button will even be disabled first.

It's therefore better to define update to itself take a callback function:

function update(callback){
    loadFile("update.php", function(){
        if (xmlhttp.readyState == 4 && xmlhttp.status == 200){
            document.getElementById("content").innerHTML = xmlhttp.responseText;
            callback();   
        }
    });
}

and then call it like this in your main code:

    global_button.disabled = true;
    update(function() {
        global_button.disabled = false;
        console.log("AFTER FUNCTION RETURN VALUES");
    });

because this separates the concerns of update itself ("make this request and set the content inner HTML to the response"), from whatever you might want to do afterwards, which could be different each time.

Finally, I can't not mention that this callback-based asynchronous code is very old-fashioned now. XMLHTTPRequest itself is quite a cumbersome API. I highly recommend you look into its modern equivalent, fetch, which is based on Promises - which while not without their mental gotchas are a much more understandable way to write asynchronous code. In particular with async and await you can write code that looks much like what you originally had: putting await update(); would actually do what you are waiting, and have the rest of your code wait for update to complete. But you can't just make that change to your original code because that only works if update returns a Promise, which in turns would mean completely rewriting your loadFile to use a more modern, Promise-based approach.

Source