DFBerry DFBerry - 2 months ago 26
Javascript Question

Changing global variable in event is not working right

I have code in plain js where the event handler changes a global variable without passing it in.

"use strict";
var outsideEventString ="hello";
console.log("before event " + outsideEventString);

const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();

/*
Assume a and b are outside your control,
but you still need to set the outsideEventString arg
*/
myEmitter.on('event', function(a, b) {
console.log(a, b, this);
console.log("inside event " + outsideEventString);
outsideEventString="goodbye";
});
myEmitter.emit('event', 'a', 'b');

// this prints out goodbye which is set in the handler only
console.log("after event " + outsideEventString);


After adding the onDOMContentLoaded event and handler to the phantomjs example netlog.js, the code looks like:

"use strict";
console.log("netlog.js");
var page = require('webpage').create(),
system = require('system'),
address;

var valueString = "hello";
var value1 = {
current: false,
timeStamp: null
};
if (system.args.length === 1) {
//console.log('Usage: phantomjs netlog.js <some URL>');
phantom.exit(1);
} else {
var PHANTOM_FUNCTION_PREFIX = '/* PHANTOM_FUNCTION */';
address = system.args[1];
page.onConsoleMessage = function(msg) {
if (msg.indexOf(PHANTOM_FUNCTION_PREFIX) === 0) {
eval('(' + msg + ')()');
} else {
console.log(msg);
}
};
page.onInitialized = function() {
// add handler & apply value1 to handler's scope'
page.evaluate(function(domContentLoaded) {
document.addEventListener('DOMContentLoaded', domContentLoaded, false);
}, page.onDOMContentLoaded.apply(this, value1));
};
// handler has access to value1 and valueString
page.onDOMContentLoaded = function(event) {
value1.current = true;
value1.timeStamp = Date.now();
valueString="goodbye";
console.log('**** DOM CONTENT LOADED ****');
};
page.onResourceReceived = function (res) {
console.log('received ' + res.id + ' ' + res.stage);
};
page.onError = function (msg, trace) {
console.log(msg);
trace.forEach(function(item) {
//console.log(' ', item.file, ':', item.line);
});
};
page.open(address, function (status) {
console.log("page opened");
if (status !== 'success') {
console.log('FAIL to load the address');
}
console.log("value = " + JSON.stringify(value1));
console.log("valueString=" + valueString);
phantom.exit();
});
}


With the code, as is, the valueString is 'goodbye' even though it isn't passed in via the '.apply(this,value1)'

The output shows

netlog.js
received 1 start
received 1 end
received 2 end
received 3 start
received 3 end
**** DOM CONTENT LOADED ****
received 5 start
received 5 end
received 6 start
received 6 end
received 7 start
received 9 start
received 8 start
received 7 end
received 9 end
received 8 end
received 4 start
received 4 end
page opened
value = {"current":true,"timeStamp":1473693805039}
valueString=goodbye
**** DOM CONTENT LOADED ****
**** DOM CONTENT LOADED ****


The valueString variable is set at the right url received id of 3 and it sticks so that I can see it inside the page.open.

Why does '**** DOM CONTENT LOADED ****' print after the page.open?

But if I change the page.onInitialized to the following, without the apply, neither value1 or valueString is set.

page.onInitialized = function() {
// add handler & apply value1 to handler's scope'
page.evaluate(function(domContentLoaded) {
document.addEventListener('DOMContentLoaded', domContentLoaded, false);
}, page.onDOMContentLoaded);
};


Why do I need to use the apply method at all to get the global variables to be set inside the handler?

Why do I only have to pass in value1 to the apply to be able to set valueString?

Answer

page.evaluate is the door to the page context in PhantomJS. Only primitive objects can be passed in. From the docs:

Note: The arguments and the return value to the evaluate function must be a simple primitive object. The rule of thumb: if it can be serialized via JSON, then it is fine.

Closures, functions, DOM nodes, etc. will not work!

So, page.onDOMContentLoaded is a function and cannot be passed into the page context. You have to pass and object into the page context and define a function there.

You can use something like this instead (using the onCallback & callPhantom pair):

page.onCallback = function(data){
    if (data.type == 'DOMContentLoaded') {
        console.log('outer: DOMContentLoaded');
    }
};
page.onInitialized = function() {
    page.evaluate(function(value1) {
        // TODO: do something with value1
        document.addEventListener('DOMContentLoaded', function(){
            console.log('inner: DOMContentLoaded');
            window.callPhantom({ type: 'DOMContentLoaded' });
        }, false);
    }, value1);
};

Why does '**** DOM CONTENT LOADED ****' print after the page.open?

Remember that Function.prototype.apply immediately executes a function. In this case, it's the page.onDOMContentLoaded function inside of page.onInitialized. So whenever the page is "initialized" you will see the result of the execution of page.onDOMContentLoaded.

Why two pages are initialized after the execution stopped is a completely different question. This is probably only some peculiarity of PhantomJS.

Comments