Thursday, August 30, 2007

global scope evaluation and DOM investigation

After precedent post I prefer to write another one about global scope evaluation and kentaromiura question:

does your evalScript proposal cause DOM side effects?


Before I post my test code that shows how a medium or big page, about 5000 divs or paragraphs, is not affected by my proposal (about 0.09 delay over 1000 evalScript interactions) I would talk about global scope evaluation.

What does it mean: global scope evaluation?
Every time we use a function to evaluate a string, temporary scope is injected into code evaluation.

While some library (in this case I talk about jQuery) uses stranges (imho) partial (wrong) solutions to remove nested scope from evaluation, I can tell You that everything You need to use a clear scope is to call another function, where this reference is just super object (window if executed inside a browser).

This is an elementary example:

function clearScopeEvaluation(data){
return eval(data);
};

// just call them everywhere ...
(function(a){
clearScopeEvaluation("a = 2");
alert([a, window.a]);
})(1);

// shows 1,2


If You use a setInterval or setTimeout function with delay === 0, You're not solving the problem because these functions are always asyncronous.
This mean that this example code will show 1 in every "delay 0" compatible browser (Opera doesn't accept 0 as delay):

var num = 1;
setTimeout(function(){num = 2}, 0);
alert(num);

// ... or if You prefere ...
var num = 1;
setTimeout("num = 2", 0);
alert(num);

That's why when You use timing or Ajax, callback will be executed at the end of every line of code, only modal events such alert, prompt or others will cause delayed function execution, if time was short enough.


// just click OK before one second or wait more to view 2 value
var num = 1;
setTimeout(function(){num = 2}, 1000);
alert("");
alert(num);

To understand better delayed functions, please read this post that shows a really basic deadlock ... just why JavaScript, if executed inside a browser, is not multi threaded.

John Resig (jQuery author) seems to know it, He implemented these functions (setTimeout and setInterval) inside Rhino (sorry John, I don't find the link) ... but remember that His implementation is based on Java threading and not on JavaScript.

For some reason there's a function inside jQery that I can't understand, called globalEval:

// Evalulates a script in a global context
// Evaluates Async. in Safari 2 :-(
globalEval: function( data ) {
data = jQuery.trim( data );
if ( data ) {
if ( window.execScript )
window.execScript( data );
else if ( jQuery.browser.safari )
// safari doesn't provide a synchronous global eval
window.setTimeout( data, 0 );
else
eval.call( window, data );
}
}

Too many concept errors for a single function ... I hope jQuery team will remove or change them as soon as they can.

Returning on simple "free scope" evaluation, there's always a limit: arguments object or sent variables should be changed by code evaluation.

function gScope(data){
eval("data=arguments=null");
};


Since global scope evaluation should be done exactly using global scope, as script tags do, my evalScript proposal does it quite perfectly and above example will not change, using my function, recieved data or arguments variables.

For people who think that code evaluation is an obsession I can just say that my evalScript proposal was wrote to load entire scripts, as dynamic appendChild(scriptWithExtSource) does, or to evaluate a bit faster big JSON server responses.

In these cases You can't say eval is evil because there aren't different way to do that if You care about speed and dynamic script import.

This is my last sentences about code evaluation, it's always a bad practice but in rare cases, it's a must.

To complete this post, here You can see my benchmark code to test evalScript speed after created a big page (big DOM):

(evalScript = function(e){
var h = evalScript.node,
s = document.createElement("script");
s.type = "text/javascript";
s.text = e;
h.appendChild(s);
h.removeChild(s);
}).node = document.getElementsByTagName("head")[0] || document.getElementsByTagName("*")[0];

onload = function(){

function create(element, content){
document.body.appendChild(
document.createElement(element)
.appendChild(
document.createTextNode(content)
).parentNode
);
};

function randomString(){
var i = Math.ceil(Math.random() * 200),
a = new Array(i);
while(i--)
a[i] = String.fromCharCode(Math.ceil(Math.random() * 40) + 30);
return a.join("");
};


for(var i = 0; i < 5000; i++)
create(i % 2 ? "div" : "p", randomString());

for(var i = 0, j = 0; i < 1000; i++){
startTime = new Date;
evalScript('endTime = new Date - startTime');
j += endTime;
};

alert(j / 1000);

};

14 comments:

Anonymous said...

Thanks a bunch!
I was just trying to make my scripts compatible with Safari, and this was just what I needed! The jQuery approach didn't work for me because of the the setTimeout...
Finaly the on-demand loading of js files now works for all major browser.

THX!!

Andrea Giammarchi said...

Just a note: jQuery has my credits inside its source code since version 1.2.2 - because it changed that function using my strategy :)

Anonymous said...

Hi! My name is Nicolas and I'm from Argentina.
Your strategy to solve this problem worked perfectly for me!

THANKS A LOT!!!

Michelle said...

You just helped me solve the 2 most difficult bugs in the project I'm working on at once!
THANK YOU!!!!

Andrew said...

Native JSON.parser coming in FF 3.1 and IE8.


var jsonString = '{"name":"Ryan", "address":"Mountain View, CA"}';
var person = JSON.parse(jsonString);


See http://ajaxian.com/archives/native-json-in-firefox-31-joins-ie-8

Andrea Giammarchi said...

I knew Andrew, anyway cheers. Just a question: did you pick up this post completely randomly? :D
We are all waiting Safari, Chrome, and Opera as well ;-)

Andrew said...

I knew you would have been hot on it - but worth a mention for others.

Not randomly, I remember you showing me this post sometime ago as i was trying to do exactly that.

Chars!

Andrea Giammarchi said...

hold on, are you "that" Andrew? Cheers mate :D

Drew said...

Its a popular name, so unsure. I have pictures to prove you were in Sopot last year and being chased down a beach at 5am with a stick held by said Andrew. :)

Andrea Giammarchi said...

Gotcha ... you are Cezary!!! :P
Take care Drew

EmanueleDG said...

I am sorry, but I don't understand why the user function must be called back this way, with eval() or the script-append trick.
Why not to use a simple
fn.apply(window, ['args']) ?

It works on every browser and it's quite easier.
Does it cause some problems?

Andrea Giammarchi said...

because for example you will have an arguments variable inside that call which could be not desired.
jQuery and many other libraries are using the same concet which is the only one truly reliable as global scope evaluation for different reasons :)

EmanueleDG said...

At first, when you spoke about the arguments variables, I thought you meant the array of arguments passed as second parameter to the apply() method, so I didn't understand.

Then, I made a lot of tests and got what you meant: if the function to be applied to the window scope contains some global variable (declared for example without the "var" constructor) in IE there's a loss of binding, that is they will not be attached to the window scope. This is not a bug of JS, but another real and serious IE error.
In other browsers infact it works fine. :(

I now understand the reason of this workaround, but another time the way IE spoils simple solutions like this disgusts me.

Thanks a lot for your nice workaround.

Cheers!

Anonymous said...

With regards to performance, I've tested your code and tried some other things and I find this approach to be generally very slow in comparison to using eval. By my admittedly crude measure it's five to six times slower.

Also, this approach also appears to suffer additional intermittent delays making it over one hundred times slower than standard eval.

Of course there are the other benefits which may make this worth using, and speed probably doesn't matter in most circumstances with small scripts, but it's definitely not fast.