admin管理员组

文章数量:1024934

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?

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?

Share Improve this question edited May 23, 2017 at 12:02 CommunityBot 11 silver badge asked May 3, 2017 at 18:40 jbyrdjbyrd 5,6459 gold badges56 silver badges94 bronze badges
Add a ment  | 

3 Answers 3

Reset to default 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 plete) 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 plete.

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/

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

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)

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?

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?

Share Improve this question edited May 23, 2017 at 12:02 CommunityBot 11 silver badge asked May 3, 2017 at 18:40 jbyrdjbyrd 5,6459 gold badges56 silver badges94 bronze badges
Add a ment  | 

3 Answers 3

Reset to default 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 plete) 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 plete.

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/

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

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)

本文标签: htmlHow to show CSS loader while syncronous JavaScript is runningStack Overflow