2011-05-27 22 views
6

Sto iniziando a lavorare su uno strumento di analisi dinamico per JS e mi piacerebbe profilare un intero ambiente in modo discreto. Fondamentalmente sto attraversando vari contesti, scavando in profondità negli oggetti, e ogni volta che colpisco una funzione, mi agganci. Ora, questo funziona relativamente bene, tranne per il fatto che si rompe quando si tratta di librerie come jQuery/prototipo eccJavascript perde il contesto durante l'aggancio in modo ricorsivo

Questo è il mio codice finora (commentato al meglio delle mie possibilità):

var __PROFILER_global_props = new Array(); // visited properties 

/** 
* Hook into a function 
* @name the name of the function 
* @fn the reference to the function 
* @parent the parent object 
*/ 
function __PROFILER_hook(name, fn, parent) { 
    //console.log('hooking ' + name + ' ' + fn + ' ' + parent); 

    if (typeof parent == 'undefined') 
     parent = window; 

    for (var i in parent) { 
     // find the right function 
     if (parent[i] === fn) { 
      // hook into it 
      console.log('--> hooking ' + name); 
       parent[i] = function() { 
         console.log('called ' + name); 
         return fn.apply(parent, arguments); 
       } 

       //parent[i] = fn; // <-- this works (obviously) 
       break; 
     } 
    } 
} 

/** 
* Traverse object recursively, looking for functions or objects 
* @obj the object we're going into 
* @parent the parent (used for keeping a breadcrumb) 
*/ 
function __PROFILER_traverse(obj, parent) { 
    for (i in obj) { 
     // get the toString object type 
     var oo = Object.prototype.toString.call(obj[i]); 
     // if we're NOT an object Object or an object Function 
     if (oo != '[object Object]' && oo != '[object Function]') { 
      console.log("...skipping " + i); 
      // skip 
      // ... the reason we do this is because Functions can have sub-functions and sub-objects (just like Objects) 
      continue; 
     } 
     if (__PROFILER_global_props.indexOf(i) == -1 // first we want to make sure we haven't already visited this property 
      && (i != '__PROFILER_global_props'  // we want to make sure we're not descending infinitely 
      && i != '__PROFILER_traverse'   // or recusrively hooking into our own hooking functions 
      && i != '__PROFILER_hook'    // ... 
      && i != 'Event'    // Event tends to be called a lot, so just skip it 
      && i != 'log'    // using FireBug for debugging, so again, avoid hooking into the logging functions 
      && i != 'notifyFirebug')) {   // another firebug quirk, skip this as well 

      // log the element we're looking at 
      console.log(parent+'.'+i); 
      // push it.. it's going to end up looking like '__PROFILER_BASE_.something.somethingElse.foo' 
      __PROFILER_global_props.push(parent+'.'+i); 
      try { 
       // traverse the property recursively 
       __PROFILER_traverse(obj[i], parent+'.'+i); 
       // hook into it (this function does nothing if obj[i] is not a function) 
       __PROFILER_hook(i, obj[i], obj); 
      } catch (err) { 
       // most likely a security exception. we don't care about this. 
      } 
     } else { 
      // some debugging 
      console.log(i + ' already visited'); 
     } 
    } 
} 

questo è il profilo e questo è il modo in invoco esso:

// traverse the window 
__PROFILER_traverse(window, '__PROFILER_BASE_'); 

// testing this on jQuery.com 
$("p.neat").addClass("ohmy").show("slow"); 

di movimento funziona bene e aggancio funziona bene fino a quando le funzioni sono semplici e non anonima (credo agganciando in funzioni anonime è impossibile, quindi non sono troppo preoccupato per questo).

Questo è un output ritagliato dalla fase di pre-elaborazione.

notifyFirebug already visited 
...skipping firebug 
...skipping userObjects 
__PROFILER_BASE_.loadFirebugConsole 
--> hooking loadFirebugConsole 
...skipping location 
__PROFILER_BASE_.$ 
__PROFILER_BASE_.$.fn 
__PROFILER_BASE_.$.fn.init 
--> hooking init 
...skipping selector 
...skipping jquery 
...skipping length 
__PROFILER_BASE_.$.fn.size 
--> hooking size 
__PROFILER_BASE_.$.fn.toArray 
--> hooking toArray 
__PROFILER_BASE_.$.fn.get 
--> hooking get 
__PROFILER_BASE_.$.fn.pushStack 
--> hooking pushStack 
__PROFILER_BASE_.$.fn.each 
--> hooking each 
__PROFILER_BASE_.$.fn.ready 
--> hooking ready 
__PROFILER_BASE_.$.fn.eq 
--> hooking eq 
__PROFILER_BASE_.$.fn.first 
--> hooking first 
__PROFILER_BASE_.$.fn.last 
--> hooking last 
__PROFILER_BASE_.$.fn.slice 
--> hooking slice 
__PROFILER_BASE_.$.fn.map 
--> hooking map 
__PROFILER_BASE_.$.fn.end 
--> hooking end 
__PROFILER_BASE_.$.fn.push 
--> hooking push 
__PROFILER_BASE_.$.fn.sort 
--> hooking sort 
__PROFILER_BASE_.$.fn.splice 
--> hooking splice 
__PROFILER_BASE_.$.fn.extend 
--> hooking extend 
__PROFILER_BASE_.$.fn.data 
--> hooking data 
__PROFILER_BASE_.$.fn.removeData 
--> hooking removeData 
__PROFILER_BASE_.$.fn.queue 

Quando eseguo $("p.neat").addClass("ohmy").show("slow"); su jQuery.com (attraverso Firebug), ottengo uno stack di chiamate del caso, ma mi sembra di perdere il mio contesto da qualche parte lungo la strada perché non succede nulla e si ottiene un errore e is undefined da jQuery (chiaramente, l'aggancio ha avvitato qualcosa).

called init 
called init 
called find 
called find 
called pushStack 
called pushStack 
called init 
called init 
called isArray 
called isArray 
called merge 
called merge 
called addClass 
called addClass 
called isFunction 
called isFunction 
called show 
called show 
called each 
called each 
called isFunction 
called isFunction 
called animate 
called animate 
called speed 
called speed 
called isFunction 
called isFunction 
called isEmptyObject 
called isEmptyObject 
called queue 
called queue 
called each 
called each 
called each 
called each 
called isFunction 
called isFunction 

Il problema è che penso che sto perdendo il contesto this quando si chiama

return fn.apply(parent, arguments); 

Ecco un altro capriccio interessante. Se collego prima che io percorro, vale a dire:

 // hook into it (this function does nothing if obj[i] is not a function) 
     __PROFILER_hook(i, obj[i], obj); 
     // traverse the property recursively 
     __PROFILER_traverse(obj[i], parent+'.'+i); 

.. l'applicazione funziona assolutamente bene, ma lo stack di chiamate viene alterato (e non sembrano avere funzioni jQuery-specifici) per qualche motivo:

called $ 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called setInterval 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called getComputedStyle 
called clearInterval 

.. invece di animation, show, merge, ecc in questo momento, tutto il gancio non è dire called functionName ma alla fine mi piacerebbe fare tracce dello stack e le funzioni di tempo (tramite un applet Java).

Questa domanda è stata enorme e mi scuso, ma qualsiasi aiuto è apprezzato!

Nota: il codice sopra potrebbe causare l'arresto anomalo del browser se non si presta attenzione. Fair warning: P

+0

Domanda simile: http://stackoverflow.com/questions/5033836/adding-console-log-to-every-function-automatically –

risposta

3

Penso che tu sia sulla strada giusta. Il valore di this si blocca quando si utilizza apply. Le funzioni definite all'interno di jQuery vengono probabilmente chiamate internamente tramite apply e dipendono dal valore di this.

Il primo argomento a apply è il valore che verrà utilizzato per this. Sei sicuro che dovresti usare parent per quello?

sono stato in grado di riprodurre il problema nel modo seguente:

var obj = { 
    fn : function() { 
     if(this == "monkeys") { 
     console.log("Monkeys are funny!"); 
     } 

     else { 
     console.log("There are no monkeys :("); 
     } 
    } 
}; 

obj.fn.apply("monkeys"); 

var ref = obj.fn; 

//assuming parent here is obj 
obj.fn = function() { 
    console.log("hooking to obj.fn"); 
    return ref.apply(obj); 
}; 

obj.fn.apply("monkeys"); 

Qui, la funzione dipende dal valore di this per stampare il testo Monkeys are funny!. Come puoi vedere, usando l'algoritmo hook, questo contesto è perso. Firebug mostra:

Monkeys are funny! 
hooking to obj.fn 
There are no monkeys :(

ho fatto un leggero cambiamento e utilizzati this nella applicare al posto del obj (genitore):

obj.fn = function() { 
    console.log("hooking to obj.fn"); 
    return ref.apply(this); 
}; 

Questa volta Firebug dice:

Monkeys are funny! 
hooking to obj.fn 
Monkeys are funny!

La radice del tuo problema IMHO è che stai impostando un valore esplicito per this (ovvero, parent che si riferisce all'oggetto padre). Quindi la funzione di hook termina sovrascrivendo il valore di this, che potrebbe essere stato impostato in modo esplicito dal codice che chiama la funzione originale. Ovviamente, quel codice non sa che hai avvolto la funzione originale con la tua funzione di hook. Così la funzione gancio deve preservare il valore della this quando si chiama la funzione originale:

return fn.apply(this, arguments); 

Quindi, se si utilizza this invece di parent nella vostra applicazione, il problema potrebbe essere risolto.

Mi scuso se non ho compreso correttamente il problema. Per favore correggimi ovunque io abbia torto.

UPDATE

Ci sono due tipi di funzioni in jQuery. Quelli che sono collegati allo stesso oggetto jQuery (una specie di metodi statici simili) e che ne hai uno che funziona sul risultato di jQuery(selector) (una specie di metodi di istanza simile). È di quest'ultima che devi preoccuparti. Qui, this conta molto perché è così che si implementa il concatenamento.

Sono riuscito a ottenere il seguente esempio per funzionare. Si noti che sto lavorando sull'istanza dell'oggetto e non sull'oggetto stesso. Quindi nel tuo esempio, avrei lavorato sulla jQuery("#someId") e non solo su jQuery:

var obj = function(element) { 
    this.element = element; 
    this.fn0 = function(arg) { 
     console.log(arg, element); 
     return this; 
    } 

    this.fn1 = function(arg) { 
     console.log(arg, arg, element); 
     return this; 
    } 

    if(this instanceof obj) { 
     return this.obj; 
    } 

    else { 
     return new obj(element); 
    } 
}; 

var objInst = obj("monkeys"); 

var ref0 = objInst.fn0; 

objInst.fn0 = function(arg) { 
    console.log("calling f0"); 
    return ref0.apply(this, [arg]); 
}; 

var ref1 = objInst.fn1; 

objInst.fn1 = function(arg) { 
    console.log("calling f1"); 
    return ref1.apply(this, [arg]); 
}; 

objInst.fn0("hello").fn1("bye"); 

Non so se questo risolve il problema o meno. Forse esaminare la sorgente di jQuery ti darà qualche informazione in più :). I pensare a che la difficoltà per voi sarebbe quella di distinguere tra funzioni chiamate tramite apply e funzioni chiamate tramite concatenamento (o chiamate direttamente).

+0

L'hai capito perfettamente, e ho provato "questo" in precedenza 'questo', sembra che le interruzioni di concatenamento di funzioni (quindi qualcosa come' $ ('qualcosa'). addClass ('asdfg'). show ('slow') 'smette di funzionare). Nello specifico, capisco che 'push()' non è definito. –

+0

@David Ho aggiornato la mia risposta.Non so se risponde alla tua domanda o no, ma ho provato: p Ho menzionato questo nella risposta, ma penso che finirai per affrontare difficoltà quando cerchi di distinguere tra le funzioni che vengono chiamate tramite 'rispetto a quelli che vengono chiamati concatenando o direttamente. Sono interessato a conoscere la soluzione anche a questo, quindi spero che qualcun altro che è più esperto di me possa intervenire. –

+0

Anche se potrebbe non risolvere il problema nel caso generale, la mia intuizione mi dice che dovrebbe essere possibile e apprezzo la tua intuizione. Penso che tu abbia colto nel segno quando hai fatto la distinzione tra il metodo di concatenazione e i metodi statici che vengono chiamati. Inoltre, facendo alcune prove ho scoperto che a volte chiamare applica gli argomenti wraps in un array di loro .. che è strano. –