Why would the difference between these two lines of code create a bug that cause jquery to loop endlessly in iOS(Safari and Chrome)? The loop did not occur in any other browser.
if ($('[name="loadingTime"]') != undefined) {...
vs
if ($('.loadingTime') != undefined) {...
When we targeted by class and not name attribute the loop bug went away. Any ideas or explanations?
Upon further investigation the bug was discovered in another part of the code. Here's what happened:
loadInterval: function() {
var settimer = $('[name="loadingTime]').val();
var interval = setInterval(function() {
if (settimer == 0) {
window.clearInterval(interval);
$('[id^="interstitial-container"]').remove();
};
settimer--;
if (settimer >= 0) {
$('.ncount').text(settimer);
}
}, 1000);
}
in
var settimer = $('[name="loadingTime]').val();
we missed a closing quote after loadingTime! which the js returned as undefined and iOS didn't handle it gracefully so var settimer wasn't set to zero so whenever that function loadInterval was called it was undefined and we checked whether we needed to load based on undefined or not. in our case it wasn't and continued to load over and over always getting an undefined response but without an error. I think...
Related
I tried to work this code:
var foo=0
window.onmouseup=function(){
foo=1
}
window.onmousedown=function(){
while(foo==0);
console.log("bar")
}
the "bar" is not shown and the browser (I use Edge) stuck there(unable to close the page), I had to use Ctrl+T and then Ctrl+W
I guess the problem is foo==0 is optimized, so it reads from the cache, but I don't know how to avoid it. Or are there other methods?
You could use setInterval() and put the if statement and the rest of the code in there:
var foo = 0
window.onmouseup = function() {
foo = 1
}
window.onmousedown = function() {
var interval = setInterval(() => {
if (foo !== 0) {
clearInterval(interval);
console.log("bar")
}
});
}
Actually, I think the problem is that your while loop will just continue running until it "breaks", or ends the loop. However, foo will always be 1 and never 0 after mouseup, therefore the program gets stuck in the while loop forever, and no other tasks on the browser including the important ones get run.
TL:DR program stuck on while
I have the following code which demonstrates the difference in calling a long-running function directly from an event trigger, vs. using setTimeout().
Intended behavior:
When the first button is pressed, it appears pressed, the calculation runs for several seconds, then when the calculation finishes, the button appears depressed again and the second column changes from "not calculating yet" to "calculation done". (I won't elaborate on why that is supposed to happen; it's explained in linked answer.)
When the second button is pressed, the button depresses immediately; the second column immediately changes to "calculating..." text. When the calculation finishes several seconds later, the second column changes from "calculating..." to "calculation done".
What actually happens:
This works perfectly in Chrome (both buttons behave as expected)
This works perfectly in Internet Explorer 8
This does NOT work in Firefox (v.25) as-is. Specifically, the second button behaves 100% as the first one.
Changing the timeout in setTimeout() from 0 to 1 has no effect
Changing the timeout in setTimeout() from 0 to 500 works
Which leaves me with a big conundrum.
According to the whole reason behind why setTimeout() works whereas lack of one doesn't, the delay should have zero effect on how things work, since setTimeout()'s main purpose is to change the queuing order here, NOT to delay things.
So, why is it not working with delay 0 or 1 on Firefox, but works as expected with delay 500 (and works with any delay on Internet Explorer 8/Chrome)?
UPDATE: In addition to source code below, I also made a JSFiddle. But for some reason JSFiddle refuses to even load on my Internet Explorer 8, so for that testing, the code below is required.
UPDATE2: Someone raised the possibility of there being an issue with configuration setting dom.min_timeout_value in Firefox. I have edited it from 4 to 0, restarted the browser, and nothing was fixed. It still fails with a timeout of 0 or 1 and succeeds with 500.
Here is my source code - I simply saved it to a HTML file on C: drive and opened in all three browsers:
<html><body>
<script src="http://code.jquery.com/jquery-1.9.1.js"></script>
<table border=1>
<tr><td><button id='do'>Do long calc - bad status!</button></td>
<td><div id='status'>Not Calculating yet.</div></td></tr>
<tr><td><button id='do_ok'>Do long calc - good status!</button></td>
<td><div id='status_ok'>Not Calculating yet.</div></td></tr>
</table>
<script>
function long_running(status_div) {
var result = 0;
for (var i = 0; i < 1000; i++) {
for (var j = 0; j < 700; j++) {
for (var k = 0; k < 200; k++) {
result = result + i + j + k;
}
}
}
$(status_div).text('calclation done');
}
// Assign events to buttons
$('#do').on('click', function () {
$('#status').text('calculating....');
long_running('#status');
});
$('#do_ok').on('click', function () {
$('#status_ok').text('calculating....');
window.setTimeout(function (){ long_running('#status_ok') }, 0);
});
</script>
</body></html>
To test, you will need to change the nested loop boundaries to 300/100/100 for Internet Explorer 8; or to 1000/1000/500 for Chrome, due to different sensitivity of "this JS is taking too long" error coupled with JavaScript engine speed.
There is a copy of the current (Jun 28, 2016) implementation of window.setTimeout() in Ubuntu.
As we can see, the timer gets inserted by this line of code:
nsAutoPtr<TimeoutInfo>* insertedInfo =
mTimeouts.InsertElementSorted(newInfo.forget(), GetAutoPtrComparator(mTimeouts));
Then a few lines below you have an if() statement:
if (insertedInfo == mTimeouts.Elements() && !mRunningExpiredTimeouts) {
...
The insertedInfo == mTimeouts.Elements() checks whether the timer that was just inserted already timed out. The following block does NOT execute the attached function, but the main loop will immediately notice that a timer timed out and thus it will skip the IDLE state (a yield of the CPU) that you are expecting.
This clearly (at least to me) explains the behavior you are experiencing. The rendering on the screen is another process (task/thread) and the CPU needs to be relinquished for that other process to get a chance to re-paint the screen. For that to happen, you need to wait long enough so your timer function does not get executed immediately and a yield happens.
As you've notice a pause of 500ms does the trick. You can probably use a smaller number, such as 50ms. Either way it is not going to guarantee that a yield happens, but chances are it will happen if the computer on which that code is running is not currently swamped (i.e. an anti-virus is not currently running full speed in the background...)
The complete SetTimeout() function from Firefox:
(location of the file in the source: dom/workers/WorkerPrivate.cpp)
int32_t
WorkerPrivate::SetTimeout(JSContext* aCx,
dom::Function* aHandler,
const nsAString& aStringHandler,
int32_t aTimeout,
const Sequence<JS::Value>& aArguments,
bool aIsInterval,
ErrorResult& aRv)
{
AssertIsOnWorkerThread();
const int32_t timerId = mNextTimeoutId++;
Status currentStatus;
{
MutexAutoLock lock(mMutex);
currentStatus = mStatus;
}
// It's a script bug if setTimeout/setInterval are called from a close handler
// so throw an exception.
if (currentStatus == Closing) {
JS_ReportError(aCx, "Cannot schedule timeouts from the close handler!");
}
// If the worker is trying to call setTimeout/setInterval and the parent
// thread has initiated the close process then just silently fail.
if (currentStatus >= Closing) {
aRv.Throw(NS_ERROR_FAILURE);
return 0;
}
nsAutoPtr<TimeoutInfo> newInfo(new TimeoutInfo());
newInfo->mIsInterval = aIsInterval;
newInfo->mId = timerId;
if (MOZ_UNLIKELY(timerId == INT32_MAX)) {
NS_WARNING("Timeout ids overflowed!");
mNextTimeoutId = 1;
}
// Take care of the main argument.
if (aHandler) {
newInfo->mTimeoutCallable = JS::ObjectValue(*aHandler->Callable());
}
else if (!aStringHandler.IsEmpty()) {
newInfo->mTimeoutString = aStringHandler;
}
else {
JS_ReportError(aCx, "Useless %s call (missing quotes around argument?)",
aIsInterval ? "setInterval" : "setTimeout");
return 0;
}
// See if any of the optional arguments were passed.
aTimeout = std::max(0, aTimeout);
newInfo->mInterval = TimeDuration::FromMilliseconds(aTimeout);
uint32_t argc = aArguments.Length();
if (argc && !newInfo->mTimeoutCallable.isUndefined()) {
nsTArray<JS::Heap<JS::Value>> extraArgVals(argc);
for (uint32_t index = 0; index < argc; index++) {
extraArgVals.AppendElement(aArguments[index]);
}
newInfo->mExtraArgVals.SwapElements(extraArgVals);
}
newInfo->mTargetTime = TimeStamp::Now() + newInfo->mInterval;
if (!newInfo->mTimeoutString.IsEmpty()) {
if (!nsJSUtils::GetCallingLocation(aCx, newInfo->mFilename, &newInfo->mLineNumber)) {
NS_WARNING("Failed to get calling location!");
}
}
nsAutoPtr<TimeoutInfo>* insertedInfo =
mTimeouts.InsertElementSorted(newInfo.forget(), GetAutoPtrComparator(mTimeouts));
LOG(TimeoutsLog(), ("Worker %p has new timeout: delay=%d interval=%s\n",
this, aTimeout, aIsInterval ? "yes" : "no"));
// If the timeout we just made is set to fire next then we need to update the
// timer, unless we're currently running timeouts.
if (insertedInfo == mTimeouts.Elements() && !mRunningExpiredTimeouts) {
nsresult rv;
if (!mTimer) {
mTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv);
if (NS_FAILED(rv)) {
aRv.Throw(rv);
return 0;
}
mTimerRunnable = new TimerRunnable(this);
}
if (!mTimerRunning) {
if (!ModifyBusyCountFromWorker(true)) {
aRv.Throw(NS_ERROR_FAILURE);
return 0;
}
mTimerRunning = true;
}
if (!RescheduleTimeoutTimer(aCx)) {
aRv.Throw(NS_ERROR_FAILURE);
return 0;
}
}
return timerId;
}
IMPORTANT NOTE: The JavaScript instruction yield, has nothing to do with what I am talking about. I am talking about the sched_yield() functionality which happens when a binary process calls certain functions, such as sched_yield() itself, poll(), select(), etc.
I faced this issue with Firefox while toggling CSS classes using jQuery to control a CSS transition.
Increasing the duration of setTimeout to 50 from 0 helped, but as Alexis suggested this wasn’t 100% reliable.
The best (if longwinded) solution I found was to combine an interval timer with an IF statement to actually check whether the necessary styles had been applied before triggering the transition, rather using setTimeout and assuming execution had taken place in the intended order, e.g.
var firefox_pause = setInterval(function() {
//Test whether page is ready for next step - in this case the div must have a max height applied
if ($('div').css('max-height') != "none") {
clear_firefox_pause();
//Add next step in queue here
}
}, 10);
function clear_firefox_pause() {
clearInterval(firefox_pause);
}
In my case at least, this seems to work every time in Firefox.
In Firefox, the minimum value for setTimeout() calls is configurable and defaults to 4 in current versions:
dom.min_timeout_value The minimum length of time, in milliseconds,
that the window.setTimeout() function can set a timeout delay for.
This defaults to 4 ms (before 10 ms). Calls to setTimeout() with a
delay smaller than this will be clamped to this minimum value.
Values like 0 or 1 should behave like 4—no idea if that will cause delays in your code or just break it.
I made this javascript method that I altered from an existing script that I found online that should rotate showing an indefinite number of '.testimonial' divs. The script works fine in chrome and firefox, but doesn't compile in internet explorer, unless you use f12 to start the debugging of the script. Is there a better way to write this script? I have looked online for ideas but haven't been able to find anything. I imagine that the issue is with the console.log(testimonialCount); statement, but am unsure of a better way to write it. Any help would be greatly appreciated. Thanks.
//rotate testimonials script
jQuery('.testimonial').hide();
var testimonialCount = $('.testimonial').length;
console.log(testimonialCount );
var currentItem = 0;
var timeout;
timeout = window.setTimeout((function(){switchDiv();}));
switchDiv = function() {
if (currentItem == testimonialCount - 1) {
jQuery('.testimonial').eq(testimonialCount - 1).hide();
currentItem = 0;
jQuery('.testimonial').eq(0).fadeIn();
timeout = window.setTimeout((function(){switchDiv();}),7000);
}
else {
jQuery('.testimonial').eq(currentItem).hide();
currentItem = currentItem + 1;
jQuery('.testimonial').eq(currentItem).fadeIn();
timeout = window.setTimeout((function(){switchDiv();}),7000);
}
}
When the IE developer tools are not open, there is no console object.
Therefore, calling console.log() throws an error.
To prevent that, you can check if ('console' in window) and make your own dummy console (or just don't log anything) if it isn't.
A few notes:
Add this to the very top of your script:
window.console = console || { 'log' : function(){} };
This defines window.console in case it isn't. Doesn't do anything except avoid errors.
Next take out the parenthesis around your timeout functions:
timeout = window.setTimeout(function(){ switchDiv(); },7000);
... or just simplify further:
timeout = window.setTimeout(switchDiv,7000);
I am being picky here but:
currentItem = currentItem + 1; is the same as currentItem++;
Another picky thing window.setTimeout is the same as setTimeout.
The following code is working correctly for all browsers including Safari on Mac, with the exception of Safari on the iPhone.
I have a timer Object that could potentially be running that is defined like so:
//delay background change until animation is finished
lastTimer = setTimeout(function () {
$('#' + targetDiv).removeClass('open');
}, 150);
Later, I need to check if the timer is running, and if so cancel it. Here is the code I am using:
if (lastTimer != null) { clearTimeout(lastTimer); }
This is where IOS Safari generates the JavaScript Error:
"ReferenceError: Can't find variable: lastTimer".
Any ideas on why the check for null is not preventing the error, like it seems to with the other browsers?
Here is the full code for the two pertaining functions in answer to the reply below: (edited with solution)
// Class for handling the animations for the drop down menus
var dropDownMenu = {
lastTimer: null,
openMenu: function (targetDiv) {
if (targetDiv != null) {
var targetHeight = $('#' + targetDiv).height();
$('#' + targetDiv).stop(true); //stop an previous animations and clear queue
if (this.lastTimer != null) { clearTimeout(this.lastTimer); } //stop possible pending timer to prevent background change
console.log("testing b");
$('#mainNavigation #dropDownMenu ul').removeClass('open'); // make sure all closed menus show corrent bgd
$('#' + targetDiv).animate({
bottom: -(targetHeight + 30)
}, 200, 'swing');
$('#' + targetDiv).addClass('open');
}
},
closeMenu: function (targetDiv) {
if (targetDiv != null) {
$('#' + targetDiv).stop(true); //stop an previous animations and clear queue
$('#' + targetDiv).animate({
bottom: 0
}, 200, 'swing');
//delay background change until animation is finished
this.lastTimer = setTimeout(function () {
$('#' + targetDiv).removeClass('open');
}, 150);
}
}
}
When the error happens in iOS the execution stops and my test console.log immediately after does not execute.
I want to chime in on this to explain. Mobile Safari is less forgiving when checking for undefined using the simple check,
if variable
When you come across situations like this then use,
if typeof variable === "undefined"
Attaching the variable to "this" is one solution here but it's just taking advantage of the fact that definedVariable.undefinedProperty returns undefined, whereas referencing an undefined variable directly will cause the reference error in some runtime environments.
I would advise not getting into the habit of attaching to "this" if it's not necessary.
There's one other potential issue that I think needs to be stated. Safari on iOS* can be very aggressive on caching and not reloading a clean copy of your code.
e.g. if you executed one version of your code without the variable defined, then fixed the code but the error continues to show on reloading (even if you close the browser/reboot the phone)
To solve this, tap and hold (aka Long Press) the reload icon in the address bar and a menu pops up with 2 options Request Desktop Site and Reload Without Content Blockers. Choosing either one of these causes a Real reload of all of the content... which resolves the issue with any cached buggy code.
*Not just Safari. Chrome on iOS (which is built on Apple's version of WebKit) can exhibit the same issue.
Your issue seems to be that on IOS, the openMenu is being invoked first.
This means that you're trying to get the value of an undeclared variable, resulting in the ReferenceError.
Oddly, you can assign to an undeclared variable, which implicitly makes it global. So if closeMenu is called first, then the assignment happens first, making the variable implicitly declared.
The proper solution is to always declare variables before using them.
var lastTimer;
But as it turned out, you preferred to use a property on the current object instead of a variable. As such, the solution was to access the property in the methods...
this.lastTimer
This will never throw a ReferenceError, even if the property is not declared.
You have a global variable in your code. Not declaring var makes it global.
Try changing:
if (lastTimer != null)
to
if (typeof lastTimer !=="undefined" && lastTimer)
if you are sticking with it being global.
In my case Safari 13 didnt work with ES6 const hence i needed to replace it with var
I'm trying to write a function that will dump a recursive tree of window for all browsers. A problem that I immediately realized I was going to have, had to do with infinite objects (window.window.window.window). Just for laughs, I tried it anyways, and I got an error as I expected. Uncaught RangeError: Maximum call stack size exceeded (testing in Chrome)
So the first approach to check against objects that were going to cause this was simply:
if (variable != 'window' && variable != 'top' && variable != 'self' && variable != 'frames')
I'm thinking maybe that would have worked, and I simply missed a couple. It was a good theory, but I still get the maximum stack error. So I decided to type window in Chrome's console, and manually look for all of the [DOMWindow] types, to add to this list. While doing that, I noticed the Infinity: Infinity value, which brought me to my next approach:
if (typeof namespace[variable]['Infinity'] === 'undefined')
I still got the maximum stack error with that, so I did a bit of Google searching, and learned about isFinite, so now I have: (edit: actually I just realized isFinite isn't what I thought it was)
if (isFinite(tree[variable]))
The error finally went away, but the problem with this approach is that all objects in window are returning false for this, so the recursion fails. I realize that some of the approaches probably aren't even cross-browser compatible, but it would be nice if I could get it to at least work in one browser in the mean time.
So how can I check for objects that are going to cause an infinite loop?
Here's my code, just for anyone who might be interested:
(function () {
window.onload = function () {
window.onload = ''; // don't want to get our own code
console.log((function (namespace) {
tree = {};
for (var variable in namespace) {
/* gonna need these later
var variable_typeof = typeof namespace[variable],
variable_object_tostring = Object.prototype.toString(namespace[variable]);
*/
//if (variable != 'window' && variable != 'top' && variable != 'self' && variable != 'frames')
//if (typeof namespace[variable]['Infinity'] === 'undefined')
if (isFinite(tree[variable]))
tree[variable] = arguments.callee(namespace[variable]);
else tree[variable] = 'Infinity';
}
return tree;
})(window)); // Start from root
}
})();
Update:
Here is a working product of what I finally came up with, for anyone interested.
GGG is worthy of mention for his help.
function loop (namespace) {
if (namespace['__infinite_test']) return '[[recursion]]'; // It's infinite
namespace['__infinite_test'] = true; // Note that we've been through this object
var tree = {};
for (var variable in namespace) {
try { // For an issue in Chrome throwing an error
namespace[variable]['__tester'] = null;
delete namespace[variable]['__tester'];
}
catch (e) {
tree[variable] = namespace[variable];
continue;
}
if (namespace.propertyIsEnumerable(variable)) tree[variable] = loop(namespace[variable]);
else tree[variable] = namespace[variable];
}
return tree;
}
console.log(loop(window));
One way to prevent infinite recursion in your problem is to keep track of a list of all objects you have already visited and if you encounter an object you've already visited, you don't recurse into it.
When you encounter an object that is not in your list, you add it to your list and then recurse into it.