4

Based on my own experience, and consistent with this answer, changes to the UI aren't made while JavaScript code is running.

Example
When I click a button "Run Script", I want a loading animation to appear, then I want some JavaScript to run, and when the JavaScript is finished running, I want the loading animation to disappear. I've created a codepen here, which (predictably) fails. The most relevant portion of code is:

$('#run-script-btn').on('click', function() {
    startLoading();
    longLoadingScript(10000);
    stopLoading();
});

startLoading() changes the CSS to display a loader, but it doesn't actually affect the UI until the JS is finished running, at which point stopLoading() is called - so essentially we never see the loading icon.

A workaround I came up with is to put a setTimeout() around the longLoadingScript() and stopLoading() code in order to give the browser a moment to actually affect the UI in the startLoading() call. The working code is in a codepen here, and the relevant portion looks like this:

$('#run-script-btn').on('click', function() {
    startLoading();
    setTimeout(function() {
        longLoadingScript(10000);
        stopLoading();
    }, 100);
});

Question
Is that a good way to do it? Is there a better / more established pattern to handle this scenario?

Community
  • 1
  • 1
jbyrd
  • 5,287
  • 7
  • 52
  • 86

3 Answers3

5

This is actually happening in your case,

1) The browser creates the Rendering Tree from HTML and CSS received from your server. It then paints that rendering tree to your window.

2) So when you make any changes in DOM such as display changes (block / none). Part (or complete) of the rendering tree need to be re-evaluated. which is called reflow.

3) Browser caches all the reflow/repaint changes and executes once the main code block execution is complete.

4) case 1 - without setTimeout: Main code block execution + reflow / repaint all changes together. (display: block and then none). No changes will be visible.

Case 2 - with setTimeout: Main code block execution + reflow(display: block) + event loop pushes setTimeout callback to the call stack - reflow will happen again (display: none)

It also works with setTimeout with 0 ms.

Answer to your question: This looks good to me and I don't see any problem using this.

$('#run-script-btn').on('click', function() {
    startLoading();
    setTimeout(function() {
        longLoadingScript(100);
        stopLoading();
    }, 0);
});

Other solution is to use offsetHeight to reflow the element. However it doesn't seem to be working in my chrome+osx platform. Works well in firefox.

$('#run-script-btn').on('click', function() {
        startLoading();
        var res = $('.page-loading').outerHeight(); // calculates offsetHeight and redraw the element
        longLoadingScript(100);
        stopLoading();
});

Ref: http://frontendbabel.info/articles/webpage-rendering-101/

Kishan Rajdev
  • 1,919
  • 3
  • 18
  • 24
  • 1
    You are wrong my friend, javascript is single thread, so running a hight CPU consuming operations like a **for..loop** or synchronous code will block everything in your web page, you won't be able to do event click. – Fernando Carvajal Jan 25 '18 at 21:57
1

This is not a short answered but I'll try to make it short.

First, JavaScript is single thread, so running blocking CPU code operations like for...loop or while...loop will block everything in your web page.

Run the snipped below to see how everything in stackoverflow will freeze for 9 seconds and you'll se too that the setInterval will stop too

setInterval(()=>{
   console.log( Math.random() )
}, 500)

setTimeout(()=>{
  blocker(9000) //This stop your entire web page for 9 seconds
}, 2500)

function blocker (ms) {
    var now = new Date().getTime();
    while(true) {
        if (new Date().getTime() > now +ms)
            return;
    }   
}
As you can see, there is nothing you can do to prevent this, that's because some webpages are very slow or keep freezing without reason.

For that ES6 have the WebWorkers

A technology that allows you to run code in the background, the problem with them is that they use static .js files that you have to write before executing them.

But, I've just created a class that abstract all of that and use generic BlobFIles for creating Generic Web Workers.

My code allow you to run scripts in the background and get its return in a resolved Promise.

class GenericWebWorker {
    constructor(...ags) {
        this.args = ags.map(a => (typeof a == 'function') ? {type:'fn', fn:a.toString()} : a)
    }

    async exec(cb) {
        var wk_string = this.worker.toString();
        wk_string = wk_string.substring(wk_string.indexOf('{') + 1, wk_string.lastIndexOf('}'));            
        var wk_link = window.URL.createObjectURL( new Blob([ wk_string ]) );
        var wk = new Worker(wk_link);

        wk.postMessage({ callback: cb.toString(), args: this.args });
 
        var resultado = await new Promise((next, error) => {
            wk.onmessage = e => (e.data && e.data.error) ? error(e.data.error) : next(e.data);
            wk.onerror = e => error(e.message);
        })

        wk.terminate(); window.URL.revokeObjectURL(wk_link);
        return resultado
    }

    async parallel(array, cb) {
        var results = []
        for (var item of [...array])
            results.push( new GenericWebWorker(item, ...this.args).exec(cb) );

        var all = await Promise.all(results)
        return all
    }

    worker() {
        onmessage = async function (e) {
            try {                
                var cb = new Function(`return ${e.data.callback}`)();
                var args = e.data.args.map(p => (p.type == 'fn') ? new Function(`return ${p.fn}`)() : p);

                try {
                    var result = await cb.apply(this, args); //If it is a promise or async function
                    return postMessage(result)

                } catch (e) { throw new Error(`CallbackError: ${e}`) }
            } catch (e) { postMessage({error: e.message}) }
        }
    }
}

function blocker (ms) {
    var now = new Date().getTime();
    while(true) {
        if (new Date().getTime() > now +ms)
            return;
    }   
}

setInterval(()=> console.log(Math.random()), 1000)

setTimeout(()=> {
  var worker = new GenericWebWorker(blocker)
  console.log('\nstarting blocking code of 10 scds\n\n')
  worker.exec(blocker => {
      blocker(10000)
      return '\n\nEnd of blocking code\n\n\n'
  }).then(d => console.log(d))
}, 4000)
If you want a short explanation of that my code does:
/*First you create a new Instance and pass all 
  the data you'll use, even functions*/
var worker = new GenericWebWorker(123, new Date(), fun1)

//And then you use them and write your code like this
worker.exec((num, date, function1) => {
    console.log(num, date)
    function1() //All of these run in backgrownd
    return 3*4

}).then(d => console.log(d)) //Print 12
Fernando Carvajal
  • 1,869
  • 20
  • 19
0

Using a promise is more understandable when read:

$('#run-script-btn').on('click', function() {
    startLoading();
    longLoadingScript(10000)
        .then(function () {
            stopLoading();
        });
});

longLoadingScript(ticks) {
    var promise = new Promise(function (resolve, reject) {
        ajax({
            success: function (value?) { promise.resolve(value); },
            fail: function (reason) { promise.reject(reason); }
        })
    });

    return promise;
}

IE (non-edge) will need a polyfill (possible polyfill)

Joe
  • 80,724
  • 18
  • 127
  • 145
  • But does this handle the ability to change the CSS animation while running JavaScript? If you give a working example, I'll accept this answer. – jbyrd Jul 20 '21 at 15:45