I'm trying to solve a quite simple task but stuck with JQuery behavior.
I have a HTML button which I disable (add disabled attribute) right after it get clicked to prevent multiple clicks, do something long running (i.e. update DOM with a lot of elements) and enable the button back.
Problem is that even the button is disabled jquery queues all clicks on it and raise my click handler right after it became enabled.
According to JQuery docs it should not raise events for a disabled element.
Bellow is my code. Open JS console, click two times on the button, notice couple 'START --' messages in the console.
<div>
<button id="mybtn" type="button">Find</button>
</div>
var btnElement = $('#mybtn');
btnElement.click(doClick);
function doClick() {
var btnElement = $('#mybtn');
btnElement.attr('disabled', true);
console.log('START --' + Date());
for (var i = 0; i < 70000; i++) {
var el = $('#mybtn');
var w = el.width();
w += 10;
}
console.log('STOP --' + Date());
el.attr('disabled', false);
}
Here is my solution http://jsfiddle.net/DRyxd/8/
var btnElement = $('#mybtn');
var buttonIsBusy = false;
function doHeavyJob () {
console.log('START --' + Date());
for (var i = 0; i < 70000; i++) {
var el = $('#mybtn');
var w = el.width();
w += 10;
}
var timeoutId = setTimeout (unblockTheButton, 0);
console.log('STOP --' + Date());
}
function unblockTheButton () {
console.log('unblockTheButton');
btnElement.attr('disabled', false);
buttonIsBusy = false;
}
function doClick() {
console.log('click', buttonIsBusy);
if (buttonIsBusy) {
return;
}
btnElement.attr('disabled', true);
buttonIsBusy = true;
var timeoutId = setTimeout (doHeavyJob, 0);
}
btnElement.click(doClick);
The issue here is that click-handler function has not finished and browser has not refreshed the DOM. That means that block was not yet applied to the button. You can try pushing your heavy code out of the current context like this:
function someHeavyCode () {
/* do some magic */
}
var timeoutId = setTimeout(someHeavyCode, 0);
This will push your heavy code out of the current context.Letting browser to update the DOM first and only after execute the heavy code.
While the heavy code is executed, browser (at least Chrome) kept the user input queue somewhere in other place (or most-likely other thread). And as soon as heavy code completes - it feeds the DOM with all that queued events. We need to ignore that events somehow. And I use the setTimeout with 0-time again. Letting the browser do what was queued before unblocking the button.
WARNING But be extremely careful with this technique. Browser will still be blocked and if you spawn a lot of such blocks it may hang.
See also this Why is setTimeout(fn, 0) sometimes useful? and consider using webworkers.
P.S. Blocking a user input in such a way is not a good approach, try to rethink what you are going to do, probably there is a better solution for that.
Related
I need to do the opposite of this post, "Best way to iterate over an array without blocking the UI"
I have to loop through hundreds of rows and set a value for each. But that job has to complete before I allow the users to do the next step and submit the updated rows to the database.
The javascript is below.
// toolbar events/actions
changeZeroDiscountButton.click(function (event) {
var value = discountComboBox.jqxComboBox('val');
if ((value != null) && (value != "")) {
value = value / 100;
// get all the rows (this may have to be changed if we have paging
var datainformations = $('#jqxgrid').jqxGrid('getdatainformation');
var rowscounts = datainformations.rowscount;
for (var i = 0; i < rowscounts; i++) {
var preSetDiscount = $("#jqxgrid").jqxGrid('getcellvalue', i, "discount");
if (preSetDiscount == .0000) {
$("#jqxgrid").jqxGrid('setcellvalue', i, "discount", value);
}
}
}
});
JavaScript is designed so it does not block the UI in any way, and this is one of its most important features for the browsers. The only exceptions are the popup message boxes (i.e. alert(), confirm(), and propmpt()). Even if it is possible, it's highly not recommended to block the UI.
There are many alternative ways to prevent the user from firing actions that shouldn't be fired until something else happens. Examples:
Disable the action's button until your processing ends then enable it back.
Set a flag (e.g. var processing = true) and check that flag in the click event of the action's button so it displays a message (e.g. "still processing, please wait...") when flag is true and execute the action when flag is false. Remember not to use alert() for the message otherwise you'll block the processing. Use a popup div instead.
Set the event handler at the beginning of the processing to a function that displays a message (e.g. "still processing, please wait...") and at the end of the processing, set the event handler to the function that will do the action. Remember not to use alert() for the message otherwise you'll block the processing. Use a popup div instead.
Show a modal popup div at the beginning of the processing with a message (e.g. "still processing, please wait..."), or progress bar, or some animation. The modal popup prevents the user from interacting with the page so they cannot click anything. For that to work, the modal popup must not have a close button or any other way to close it. At the end of processing, close the modal popup so the user can now continue.
Important Note: You mentioned in your comment to the other answer that the overlay (which is similar to the modal popup in my last point) is not displayed until the end of processing. That's because your processing is occupying the processor and preventing it from handling the UI thread. When you can do is delay your processing. So first display the modal popup (or overlay), then use setTimeout() to start processing 1 second later (maybe 500 millisecond or even less is enough). This gives the processor enough time to handle the UI thread before it starts your long processing.
Edit Here is an example of the last method:
function start() {
disableUI();
setTimeout(function() {
process();
}, 1000);
}
function process() {
var s = (new Date()).getTime();
var x = {};
for (var i = 0; i < 99999; i++) {
x["x" + i] = i * i + i;
}
var e = new Date().getTime();
$("#results").text("Execution time: " + (e - s));
enableUI();
}
function disableUI() {
$("#uiOverlay").dialog({
modal: true,
closeOnEscape: false,
dialogClass: "dialog-no-close",
});
}
function enableUI() {
$("#uiOverlay").dialog("close");
}
.dialog-no-close .ui-dialog-titlebar {
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
<button type="button" onclick="start()">Start</button>
<div id="results"></div>
<div id="uiOverlay" style="display: none;">Processing... Please wait...</div>
Edit 2 Here is an example of the third method:
$("#StartButton").on("click", start);
function start() {
//remove all previous click events
$("#StartButton").off("click");
//set the click event to show the message
$("#StartButton").on("click", showProcessingMsg);
//clear the previous results
$("#results").text("");
setTimeout(function() {
process();
}, 1000);
}
function process() {
var s = (new Date()).getTime();
var x = {};
for (var i = 0; i < 99999; i++) {
x["x" + i] = i * i + i;
}
var e = new Date().getTime();
$("#results").text("Execution time: " + (e - s));
//remove all previous click events
$("#StartButton").off("click");
//set the click event back to original
$("#StartButton").on("click", start);
}
function showProcessingMsg() {
$("#results").text("Still processing, please wait...");
}
.dialog-no-close .ui-dialog-titlebar {
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button type="button" id="StartButton">Start</button>
<div id="results"></div>
If it is a long loop browser by itself will block all other events. You may just experience a freeze browser.
That wont be a good experience.
You can look cover the UI with an Overlay like this and inform user about the operation
I'm trying to set event listeners but it's only working if I set them within setTimeout.
Doesn't work:
WebApp.setController('jobs', function() {
WebApp.setView('header', 'header');
WebApp.setView('nav', 'nav');
WebApp.setView('jobs', 'main');
var jobs = document.querySelectorAll('.jobs-category');
for(let i = 0; i < jobs.length; i++)
{
console.log('events added');
jobs[i].addEventListener("dragover", function( event ) {
console.log('drag over');
event.preventDefault();
});
jobs[i].addEventListener('drop', function(event) {
event.preventDefault();
console.log('dropped');
}, false);
}
});
Does work:
WebApp.setController('jobs', function() {
WebApp.setView('header', 'header');
WebApp.setView('nav', 'nav');
WebApp.setView('jobs', 'main');
window.setTimeout(function() {
var jobs = document.querySelectorAll('.jobs-category');
for(let i = 0; i < jobs.length; i++)
{
console.log('events added');
jobs[i].addEventListener("dragover", function( event ) {
console.log('drag over');
event.preventDefault();
});
jobs[i].addEventListener('drop', function(event) {
event.preventDefault();
console.log('dropped');
}, false);
}
}, 1);
});
(only setTimout is different/additionally)
setController() saves the function and executes it if the route get requested.
setView() binds HTML5-templates to DOM:
var Template = document.querySelector('#' + Name);
var Clone = document.importNode(Template.content, true);
var CloneElement = document.createElement('div');
CloneElement.appendChild(Clone);
CloneElement = this.replacePlaceholders(CloneElement);
document.querySelector(Element).innerHTML = CloneElement.innerHTML;
Why does this only work in setTimeout? I thought javascript is synchronous.
Addition: This is a single page app, which gets already loaded after DOM is ready.
Where is your Javascript within the page?
My guess is that the HTML is not ready yet when you try to register the event, this is why it only works with setTimeout.
Try either including your javascript at the bottom of the page (after the HTML) or listening to the page loaded event. See this question for info how to do that - $(document).ready equivalent without jQuery.
Per your assumption of synchronous javascript - yes it is (mostly) but also the way that the browser renders the page is (unless using async loading of scripts). Which means that HTML that is after a javascript will not be available yet.
UPDATE - per your comment of using a single page app, you must listen to the load complete/done/successful event. Another way you could bypass it will be listening to the events on the parent element (which is always there).
Hope this helps.
document.addEventListener("DOMContentLoaded", function(event) {
//do work
});
Most likely you are loading your JS file before your html content. You can't run your Javascript functions until the DOM is ready.
Forgive my naivety, this probably is quite obvious, I just can't see it now.
Please tell me what is wrong with the following code:
$('#iframe1').load(function(){
$('#iframe2').load(function(){
alert('loaded!');
});
});
The idea is to wait until both iframes have fully loaded, then alert "loaded" - of course this is a simplified example for the sake of stack.
The script sits in script tags at the end of the body of the html doc.
#Quertiy answer is perfectly fine, but not very jQuery-ish. It is hard-coded for 2 iframes only.
The beauty of jQuery is that you can make it work for the most number of people, with as little friction as possible.
I've advised a very simplistic plugin that does nearly what is present on that answer, but in a more open way. It not only works on iframes, but also on images, audio, video and whatever has a onload event!
Without further due, here's the code:
(function($){
$.fn.extend({allLoaded: function(fn){
if(!(fn instanceof Function))
{
throw new TypeError('fn must be a function');
}
var $elems = this;
var waiting = this.length;
var handler = function(){
--waiting;
if(!waiting)
{
setTimeout(fn.bind(window), 4);
}
};
return $elems.one('load.allLoaded', handler);
}});
})(window.jQuery);
It works by adding a load handler to every element in that selection. Since it is a plugin, you can use in whatever way you decide to use it.
Here's an example, that loads 30 random images:
//plugin code
(function($){
$.fn.extend({allLoaded: function(fn){
if(!(fn instanceof Function))
{
throw new TypeError('fn must be a function');
}
var $elems = this;
var waiting = this.length;
var handler = function(){
--waiting;
if(!waiting)
{
setTimeout(fn.bind(window), 4);
}
};
return $elems.one('load.allLoaded', handler);
}});
})(window.jQuery);
$(function(){
//generates the code for the 30 images
for(var i = 0, html = ''; i < 30; i++)
html += '<img data-src="http://lorempixel.com/g/400/200/?_=' + Math.random() + '">';
//stuffs the code into the body
$('#imgs').html(html);
//we select all images now
$('img')
.allLoaded(function(){
//runs when done
alert('loaded all')
})
.each(function(){
//the image URL is on a `data` attribute, to delay the loading
this.src = this.getAttribute('data-src')
})
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.0/jquery.min.js"></script>
<div id="imgs"></div>
Your problem, as said before many times, is that you have a load event attached to your iframe. That event is fired everytime the content change.
After that, you set a new event on #iframe2. When it's content changes, it will fire events left and right, above and beyound what you wish!
The best aproach is to keep track of which ones you loaded or not. After all have been loaded, you simply run the function.
The problem is that you're waiting until #iframe1 loads before you attach a handler for #iframe2 loading. So if #iframe2 loads first, you'll never get your callback.
Instead, watch the load event on both of them and track which ones you've seen:
var seen1 = false,
seen2 = false;
$('#iframe1, #iframe2').load(function(){
if (this.id == "iframe1") {
seen1 = true;
} else {
seen2 = true;
}
if (seen1 && seen2) {
alert('loaded!');
}
});
Why do you expect 2nd iframe to load after the first one?
~function () {
var loaded = 0;
$('#iframe1, #iframe2').load(function (){
if (++loaded === 2) {
alert('loaded!');
}
});
}()
I'm having a bit of a jquery javascript performance issue, specifically related to Firefox.
We have a set of vimeo embeds, and the ids are pulled in via a json file. On each click, a new video is displayed. After the video is played, the container is removed and the title cloud is put back in. After a certain number of rounds, Firefox performance seriously degrades and you get the "unresponsive script" error. This isn't happening on any other browsers. Furthermore, the profiler in FF doesn't seem to point to a root cause of the slowdown.
I believe this is caused by poor iframe performance and how FF handles iframes, but I'm not entirely sure about this. Nothing else I'm doing is anything too, mostly just stock jquery functions like empty(), remove(), prepend(), etc.
I have implemented a click counter which will just refresh the page after a certain amount of click throughs. This resolved the problem, but it's a hacky solution which I seriously dislike. I would love some ideas on the root cause of this and any advice on how to solve it.
Here's the link to the site and the specific portion mentioned:
http://www.wongdoody.com/mangles
This isn't all the code, but this is the part that gets called every click.
Also, I have tried just swapping out the src="" in the iframe, but performance still degrades.
EDIT: I can confirm this is not a memory leak, I used about:memory and with addons disabled in safe mode I'm getting decent memory usage:
359.11 MB ── private
361.25 MB ── resident
725.54 MB ── vsize
Something in the vimeo embed is slowing down the javascript engine, but it's not a memory leak. Also, this is confirmed by the fact that I can resolve the issue by just refreshing the page. If it was a memory leak I would have to close FF altogether.
function getIframeContent(vid) {
mangle_vid_id = vid;
return '<div class="vimeoContainerflex"><div class="vimeoContainer"><iframe class="vimeo" style="z-index:1;" width="100%" height="100%" frameborder="0" allowfullscreen="" mozallowfullscreen="" webkitallowfullscreen="" src="//player.vimeo.com/video/' + mangle_vid_id + '?api=1&title=0&color=89ff18&byline=0&portrait=0&autoplay=1"></iframe></div></div>';
}
function show_titles() {
$('.mangle-btn').hide();
$('.vimeoContainerflex').remove();
$('span.mangle').hide();
if ($('#mangle-titles').length < 1) {
$('#wongdoody').prepend(wd_titles_content);
}
$('#arrow').show();
if (clicks > 12) {
location.reload();
}
$('#mangle-titles span').click(function() {
clicks = clicks + 1;
$('#mangle-wrapper').remove();
var vidID = $(this).attr('data-id');
if ($('.vimeoContainer').length < 1) {
if (vidID == "home") {
$('#wongdoody').prepend(getIframeContent(getRandom()));
} else {
$('#wongdoody').prepend(getIframeContent(vidID));
}
}
$('#arrow').hide();
vimeoAPI();
});
$('#mangle-titles span').not('noscale').each(function() {
var _this = $(this);
var classname = _this.attr('class');
var scaleNum = classname.substr(classname.length - 2);
var upscale = parseInt(scaleNum);
var addition = upscale + 5;
var string = addition.toString();
_this.hover(
function() {
_this.addClass('scale' + string);
},
function() {
_this.removeClass('scale' + string);
}
);
});
}
function vimeoAPI() {
var player = $('iframe');
var url = window.location.protocol + player.attr('src').split('?')[0];
var status = $('.status');
// Listen for messages from the player
if (window.addEventListener) {
window.addEventListener('message', onMessageReceived, false);
} else {
window.attachEvent('onmessage', onMessageReceived, false);
}
// Handle messages received from the player
function onMessageReceived(e) {
var data = JSON.parse(e.data);
switch (data.event) {
case 'ready':
onReady();
break;
case 'finish':
onFinish();
break;
}
}
// Helper function for sending a message to the player
function post(action, value) {
var data = {
method: action
};
if (value) {
data.value = value;
}
var message = JSON.stringify(data);
if (player[0].contentWindow != null) player[0].contentWindow.postMessage(data, url);
}
function onReady() {
post('addEventListener', 'finish');
}
function onFinish() {
setTimeout(show_titles, 500);
}
}
Part of you're problem may be the fact that you keep adding more and more click-handlers to the spans. After each movie ends the onFinish function calls show_titles again, which attaches a new (=additional) click-handler to the $('#mangle-titles span') spans. jQuery does not remove previously attached handlers.
Try splitting the show_titles function into two. init_titles should be called only once:
function init_titles() {
if ($('#mangle-titles').length < 1) {
$('#wongdoody').prepend(wd_titles_content);
}
$('#mangle-titles span').click(function() {
$('#mangle-wrapper').remove();
var vidID = $(this).attr('data-id');
if ($('.vimeoContainer').length < 1) {
if (vidID == "home") {
$('#wongdoody').prepend(getIframeContent(getRandom()));
} else {
$('#wongdoody').prepend(getIframeContent(vidID));
}
}
$('#arrow').hide();
vimeoAPI();
});
$('#mangle-titles span').not('noscale').each(function() {
var _this = $(this);
var classname = _this.attr('class');
var scaleNum = classname.substr(classname.length - 2);
var upscale = parseInt(scaleNum);
var addition = upscale + 5;
var string = addition.toString();
_this.hover(
function() {
_this.addClass('scale' + string);
},
function() {
_this.removeClass('scale' + string);
}
);
});
}
function show_titles() {
$('.mangle-btn').hide();
$('.vimeoContainerflex').remove();
$('span.mangle').hide();
$('#arrow').show();
}
I'd recommend trying to re-use the iframe instead of wiping and re-adding. Failing that, I think you may be out of luck. Your method of closing the iFrame is fine; your browser that it's running in is not.
You're overloading window with eventListeners. Each time a user clicks a video, you're attaching an event to window that fires every time you're receiving a message.
You can easily check this by adding console.log("Fire!"), for instance, at the beginning of onMessageReceived. You'll see that this function gets triggered an awful number of times after the user has performed some clicks on videos.
That surely has an impact on performance.
Hope this helps.
Ok, firstly, I hardly know Javascript. I really don't know what I'm doing.
So, I have this code:
var interval_id = 0;
var prevent_bust = 0;
// Event handler to catch execution of the busting script.
window.onbeforeunload = function() { prevent_bust++ };
// Continuously monitor whether busting script has fired.
interval_id = setInterval(function() {
if (prevent_bust > 0) { // Yes: it has fired.
prevent_bust -= 2; // Avoid further action.
// Get a 'No Content' status which keeps us on the same page.
window.top.location = 'http://vadremix.com/204.php';
}
}, 1);
function clear ()
{
clearInterval(interval_id);
}
window.onload="setTimeout(clear (), 1000)";
After 1 second I want to clear the interval set earlier. This isn't working. How would I do what I'm trying to do?
If you substitute the last line with window.onload = function() { setTimeout(clear, 1000); }, it should do OK.
There are two errors in your code:
window.onload should be a function, rather than a string ("..."),
setTimeout accepts a function (clear), rather than the result from the function (clear())
By the way, these are some good places to learn JavaScript:
QuirksMode
Mozilla Developer Network