How to show CSS loader while syncronous JavaScript is running

≡放荡痞女 提交于 2020-08-23 04:11:21

问题


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?


回答1:


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/




回答2:


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



回答3:


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)



来源:https://stackoverflow.com/questions/43767722/how-to-show-css-loader-while-syncronous-javascript-is-running

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!