I was toying (read: learning) around with Javascript and came across something to my understanding, seems very odd. It has to do with closures and a reference that seems to 'loose' its importance to the browser.
The browser I am using is Chromium 5.0.307.7.
Anyway, here's some code:
HTMLElement.prototype.writeInSteps = function() {
var i = 0;
var elem = this;
var args = arguments;
function step() {
elem.innerHTML += args[i];
if(i < args.length) {
i += 1;
} else {
elem.innerHTML = "";
i = 0;
}
setTimeout(step, 500);
}
step();
}
What happens here is that the first argument gets written to the correct HTMLElement, but all the ones after does not. What seems to happen is that after the first argument, the following arguments are written to some other element that is now being referenced by 'elem'.
I should also mention that, this only seems to happen when I write something directly after calling this function, like this:
div.writeInSteps("This", " is", " not", " working");
$id("body").innerHTML += "Doh!";
If I refrain from writing anything after calling this function, it seems to work ok.
If I instead change the above code to:
HTMLElement.prototype.writeInSteps = function() {
var i = 0;
var e = this.id;
var args = arguments;
function step() {
var elem = $id(e);
elem.innerHTML += args[i];
if(i < args.length) {
i += 1;
} else {
elem.innerHTML = "";
i = 0;
}
setTimeout(step, 500);
}
step();
}
Everything is dandy. My question is, what's really happening behind the scenes in the first version?
EDIT: Updated with requested details about "...write something directly after..." and browser usage as requested by ntownsend. Bryan Matthews, I'm not sure how to provide a test page without making this question overly cluttered though.
I suspect this is a DOM issue, not a JavaScript issue.
My guess is that something's mutating an ancestor of the element to which you're trying to write in steps. For example, if innerHTML of the element's parent is set (even to the exact same string, I think), the element reference you have will be to an element that's no longer in the DOM. Re-getting the element by ID each time would work around that problem.
If you're replacing the innerHTML of an ancestor of elem (the body element, as per your example), then elem no longer exists. When step is after the original elem is destroyed, what elem is referencing is going to be something else.
The correct thing for the browser to do should probably be to remove the elem reference, but it doesn't look like it's doing that.
my guess is, execution of setTimeout(step, 500); callback has very little idea who this is - whether you're calling current element's step or maybe HTMLElement.prototype.writeInSteps.step() ?
Try your second code on two different elements simultaneously, (so that second writeInSteps comes before first timeout). I'm pretty sure it won't do quite what you expect.
Of course Jeff can be right too. innerHTML is read-only by specification, and writing to it may or may not rebuild the whole tree (and kill all the references).
Related
This question stems from a conundrum I am facing in Javascript, though a more general scientific response would be extremely helpful.
If an object or array is being iterated over for another purpose—and it is known that only one element of interest has changed which can be acted upon for manipulation—is it best to:
Simply replace every element with new data to reflect the change
Rigorously check each element and replace only that which has changed
(In this example, heights of all bars of a graph are being adjusted—as they are relative—though only one textual piece of information is targeted for change.)
Array.from(result['data']).forEach(row => {
const bar = document.getElementById('bar-' + row['date']);
bar.style.height = 'calc(1.6rem + ' + row['percentage'] + '%)';
bar.firstChild.textContent = row['distance'];
});
Or:
Array.from(result['data']).forEach(row => {
const bar = document.getElementById('bar-' + row['date']);
bar.style.height = 'calc(1.6rem + ' + row['percentage'] + '%)';
if (bar.firstChild.textContent !== row['distance']) bar.firstChild.textContent = row['distance'];
});
I suppose this is a question that exposes my ignorance and it has made it difficult for me to research a conclusion: Is it more computationally exhausting to replace all elements when a difference is known to exist somewhere in the set, or is it cheaper to seek out the offending individual and change only that value?
(Setting timers, i.e. console.timeEnd(), has proved inconclusive.)
Any education would be throughly appreciated. I can't get my head around it.
It depends on the browser.
On Chrome and Opera, at least, plain assignment without checking looks to be more performant than looking up the existing text, even without possible assignment on top of looking up the existing text, by an order of around 3x:
(warning: running the following code will block your browser for some time, only press "Run" if you're sure)
const fn1 = () => {
const bar = document.querySelector('#bar');
for (let i = 0; i < 9999999; i++) bar.textContent = 'bar1';
};
const fn2 = () => {
const bar = document.querySelector('#bar');
for (let i = 0; i < 9999999; i++) {
// The following condition will never be fulfilled:
if (bar.textContent !== 'bar2') bar.textContent = 'bar2';
}
};
const now0 = performance.now();
fn1();
const now1 = performance.now();
fn2();
const now2 = performance.now();
console.log(now1 - now0);
console.log(now2 - now1);
<div id="bar"></div>
On the other hand, on Firefox 56, the lookup seems to take next to no time at all (whereas assignment is computationally expensive)
But this is only really something to worry about if you have tons and tons of elements. Unless you're dealing with thousands or tens of thousands of elements, it's not something worth optimizing for.
It's not necessary to check if the property already has the value you're assigning to it. The browser will determine if the value actually changed and handle it accordingly.
Since in your example you have already called DOM method getElementById which is the slowest part checking the property is way faster than performing a change to it. So it's always better to keep DOM-manipulations as little as possible.
UPD #CertainPerformance's test shows that performance varies amongst browsers =)
I'm using this function to append new items in order by the amount. This function is being called every 30-50ms.
var insertBefore = false;
container.find('.roll-user-row[data-user-id="' + user_data.id + '"]').remove();
container.children().each(function () {
var betContainer = $(this), itemAmount = $(this).attr('data-amount'), betId = $(this).attr('data-user-id');
if (itemAmount < betData.totalAmount) {
insertBefore = betContainer;
return false;
}
});
if (insertBefore) {
$(template).insertBefore(container);
} else {
container.prepend(template);
}
itemAmount = $(this).attr('data-amount') is integer, betData.totalAmount is interger too. And if appending goes slower than ±300ms - everything works well. In case of fast appending I get this result:
and thats not even close what I want - thats random. How to solve this?
1. Refactoring
First of all, return within .each callback doesn't work. It just breaks current iteration, not all the cycle. If you want to interrupt cylce, you should use simple for-loop and break statement. Then, I would recommend to call $() as rarely as possible, because this is expensive. So I would suggest the following refactoring for your function:
function run() {
container.find('.roll-user-row[data-user-id="' + user_data.id + '"]').remove();
var children = container.children();
for (var i = 0; i < children.length; i++) {
var betContainer = $(children[i]); // to cache children[i] wrapping
var itemAmount = betContainer.attr('data-amount');
var betId = betContainer.attr('data-user-id');
if (itemAmount < betData.totalAmount) {
$(template).insertBefore(container);
return; // instead of "break", less code for same logic
}
}
container.prepend(template); // would not be executed in case of insertBefore due to "return"
}
2. Throttling
To run a 50ms repeating process, you are using something like setInterval(run, 50). If you need to be sure, that run is done and this is 300ms delay, then you may use just setInterval(run, 300). But if the process initializes in a way that you can't change, and 50ms is fixed interval for that, then you may protect run calling by lodash throttle or jquery throttle plugin:
var throttledRun = _.throttle(run, 300); // var throttledRun = $.throttle(300, run);
setInterval(throttledRun, 50);
setInterval is just for example, you need to replace your initial run with throttled version (throttledRun) in your repeater initialization logic. This means that run would not be executed until 300ms interval has passed since the previous run execution.
I am only posting the approach here, if my understanding is right, then I'll post a code. First thing came to my mind reading this was the 'Virtual DOM' concept. Here is what you can do,
Use highly frequent random function calls only to maintain a data structure like an object. Don't rely on DOM updates.
Then use a much less frequent setInterval repetitive function call to redraw (or update) your DOM from that data structure.
I am not sure there are any reason you can't take this approach, but this will be the most efficient way to handle DOM in a time critical use-case.
I have a game I'm creating where lights run around the outside of a circle, and you must try and stop the light on the same spot three times in a row. Currently, I'm using the following code to loop through the lights and turn them "on" and "off":
var num_lights = 20;
var loop_speed = 55;
var light_index = 0;
var prevent_stop = false; //If true, prevents user from stopping light
var loop = setTimeout(startLoop, loop_speed);
function startLoop() {
prevent_stop = false;
$(".light:eq(" + light_index + ")").css("background-color", "#fff");
light_index++;
if(light_index >= num_lights) {
light_index = 0;
}
$(".light:eq(" + light_index + ")").css("background-color", "red");
loop = setTimeout(startLoop, loop_speed);
}
function stopLoop() {
clearTimeout(loop);
}
For the most part, the code seems to run pretty well, but if I have a video running simultaneously in another tab, the turning on and off of the lights seems to chug a bit. Any input on how I could possibly speed this up would be great.
For an example of the code from above, check out this page: http://ericditmer.com/wheel
When optimizing the thing to look at first is not doing twice anything you only need to do once. Looking up an element from the DOM can be expensive and you definitely know which elements you want, so why not pre-fetch all of them and void doing that multiple times?
What I mean is that you should
var lights = $('.light');
So that you can later just say
lights.eq(light_index).css("background-color", "red");
Just be sure to do the first thing in a place which keeps lights in scope for the second.
EDIT: Updated per comment.
I would make a global array of your selector references, so they selector doesn't have to be executed every time the function is called. I would also consider swapping class names, rather than attributes.
Here's some information of jQuery performance:
http://www.componenthouse.com/article-19
EDIT: that article id quite old though and jQuery has evolved a lot since. This is more recent: http://blog.dynatrace.com/2009/11/09/101-on-jquery-selector-performance/
You could try storing the light elements in an array instead of using a selector each time. Class selectors can be a little slow.
var elements = $('.light');
function startLoop() {
prevent_stop = false;
$(elements[light_index]).css('background-color', '#fff');
...
}
This assumes that the elements are already in their intended order in the DOM.
One thing I will note is that you have used a setTimeout() and really just engineered it to behave like setInterval().
Try using setInterval() instead. I'm no js engine guru but I would like to think the constant reuse of setTimeout has to have some effect on performance that would not be present using setInterval() (which you only need to set once).
Edit:
Curtousy of Diodeus, a related post to back my statement:
Related Stack Question - setTimeout() vs setInterval()
OK, this includes some "best practice" improvements, if it really optimizes the execution speed should be tested. At least you can proclaim you're now coding ninja style lol
// create a helper function that lend the array reverse function to reverse the
// order of a jquery sets. It's an object by default, not an array, so using it
// directly would fail
$.fn.reverse = Array.prototype.reverse;
var loop,
loop_speed = 55,
prevent_stop = false,
// prefetch a jquery set of all lights and reverses it to keep the right
// order when iterating backwards (small performance optimization)
lights = $('.light').reverse();
// this named function executes as soon as it's initialized
// I wrapped everything into a second function, so the variable prevent_stop is
// only set once at the beginning of the loop
(function startLoop() {
// keep variables always in the scope they are needed
// changed the iteration to count down, because checking for 0 is faster.
var num_lights = light_index = lights.length - 1;
prevent_stop = false;
// This is an auto-executing, self-referencing function
// which avoids the 55ms delay when starting the loop
loop = setInterval((function() {
// work with css-class changing rather than css manipulation
lights.eq( light_index ).removeClass('active');
// if not 0 iterate else set to num_lights
light_index = (light_index)? --light_index:num_lights;
lights.eq( light_index ).addClass('active');
// returns a referenze to this function so it can be executed by setInterval()
return arguments.callee;
})(), loop_speed);
})();
function stopLoop() {
clearInterval(loop);
}
Cheers neutronenstern
i'm implementing a charcounter in the UI, so a user can see how many characters are left for input.
To count, i use this simple function:
function typerCount(source, layerID)
{
outPanel = GetElementByID(layerID);
outPanel.innerHTML = source.value.length.toString();
}
source contains the field which values we want to meassure
layerID contains the element ID of the object we want to put the result in (a span or div)
outPanel is just a temporary var
If i activate this function, while typing the machine really slows down and i can see that FF is using one core at 100%. you can't write fluently because it hangs after each block of few letters.
The problem, it seems, may be the value.length() function call in the second line?
Regards
I can't tell you why it's that slow, there's just not enough code in your example to determine that. If you want to count characters in a textarea and limit input to n characters, check this jsfiddle. It's fast enough to type without obstruction.
It could be having problems with outPanel. Every time you call that function, it will look up that DOM node. If you are targeting the same DOM node, that's very expensive for the browser if it's doing that every single time you type a character.
Also, this is too verbose:
source.value.length.toString();
This is sufficient:
source.value.length;
JavaScript is dynamic. It doesn't need the conversion to a string.
I doubt your problem is with the use of innerHTML or getElementById().
I would try to isolate the problem by removing parts of the function and seeing how the cpu is used. For instance, try it all these ways:
var len;
function typerCount(source, layerID)
{
len = source.value.length;
}
function typerCount(source, layerID)
{
len = source.value.length.toString();
}
function typerCount(source, layerID)
{
outPanel = GetElementByID(layerID);
outPanel.innerHTML = "test";
}
As artyom.stv mentioned in the comments, cache the result of your GetElementByID call. Also, as a side note, what is GetElementByID doing? Is it doing anything else other than calling document.getElementById?
How would you cache this you say?
var outPanelsById = {};
function getOutPanelById(id) {
var panel = outPanelsById[id];
if (!panel) {
panel = document.getElementById(id);
outPanelsById[id] = panel;
}
return panel;
};
function typerCount(source, layerId) {
var panel = getOutPanelById(layerId);
panel.innerHTML = source.value.length.toString();
};
I'm thinking there has to be something else going on though, as even getElementById calls are extremely fast in FF.
Also, what is "source"? Is it a DOMElement? Or is it something else?
I am trying to dynamically change an element's onClick event and I have something like the following:
for (var i = 1; i < 5; i++)
{
getElementById('element' + i).onclick = function() { existingFunction(i); return false; };
}
Everything seems to work fine apart from the fact that the argument passed to 'existingFunction()' is the final value of i=4 each time it is called. Is there a way to bind a function to onclick that uses the value of i at the time of binding as opposed to what it seems to be doing at the moment and referencing the original i in the for-loop.
Also is is there a way of performing the same bind without having to create anonymous functions each time? so that I can directly reference 'existingFunction' in each onclick for performance reasons?
Cheers guys,
Yong
Change
for (var i = 1; i < 5; i++)
{
getElementById('element' + i).onclick = function() { existingFunction(i); return false; };
}
to
for (var i = 1; i < 5; i++)
{
getElementById('element' + i).onclick = createOneHandler(i);
}
function createOneHandler(number){
return function() {
existingFunction(number);
}
}
and it should work fine.
Working Demo
A good explanation is given here
JavaScript, time to grok closures
for the i being always 4, you have a scoping problem, I advise to read this. Scoping is are really important concept, so you have better to make sure to understand what's is going on.
a better code would be
for (var i = 1; i < 5; i++)
{
getElementById('element' + i).onclick = existingFunction;
}
the onclick would pass an event has argument so you can know what element have been clicked
i.e.
function existingFunction(event){
// DO something here
}
you can read more about events there. IE does have the exact same event model as other browser so you would have to handle it.
Last bit, I advise you to use a JS framework(Jquery,ExtJS,DOJO,Prototype...) because it would simplify your task
the code you posted should work the way you intended, your problem with i=4 is elsewhere. edit: this is wrong, rageZ is right about the scoping problem.
re the other question: all you can do is offload the verbosity with
var f = function (i) { return function () { existingFunction(i); return false; } }
for (...) { document.getElementById(...).onclick = f(i); }
BTW, you should use something like jQuery for DOM manipulation (concise syntax), and perhaps Zeta (http://codex.sigpipe.cz/zeta/) for the function composition
var f = compose(false_, existingFunction);
for (...) { $(...).click(f(i));
Hooray! It's loop closures again! See 422784, 643542, 1552941 et al for some more discussion.
is there a way of performing the same bind without having to create anonymous functions each time?
Yes, in ECMAScript Fifth Edition you get function.bind:
for (var i = 1; i < 5; i++)
document.getElementById('element'+i).onclick= existingFunction.bind(window, i);
In the meantime since browsers don't yet generally support it you can monkey-patch an alternative implementation of bind (see the bottom of this comment for one such) built out of anonymous functions as a fallback.
Alternatively, assign the same event handler function to every element and just have it look at this.id to see which element number it is.