Browser: Google Chrome (Win 10x64)
This is the first time I am using the postMessage
API of javascript hence I am not aware of all of its nuances.
I am trying to loop over a set of DOM elements and then open up a bunch links in different tabs. Once those tabs are open find certain elements in their respective DOMs and display them via postMessage()
in the main windows.
The problem is that even though I have several tabs open, I only get information from the last tab.
Here is my code:
Prepare postMessage()
listener on main window:
window.addEventListener('message', (event) => { console.log(event.data); }, false);
Create my tabs array and get my DOM elements:
alert_list = document.querySelectorAll("tr.expand.alerts-table-tablerow");
var newTabs = [];
Loop over DOM elements, open tabs, add JS code that calls back to main window with the required data. This is where the main work happens:
alert_list.forEach((currentValue, currentIndex) => {
alert_status = currentValue.childNodes[13].innerText;
if(alert_status == "Enabled") {
console.log(currentValue.childNodes[3].innerText + " " + currentValue.childNodes[7].innerText);
newTabs.push(window.open(currentValue.childNodes[5].children[0].href, "_blank"));
newTabs[newTabs.length - 1].onload = function() {
setTimeout(((tab_element) => {return () => {
window.parent.postMessage(tab_element.document.querySelectorAll("h1.search-name.section-title.search-title-searchname")[0].innerText);
}})(newTabs[newTabs.length - 1]), 120*1000);
};
}
} );
Now I keep getting information from the last tab that is opened multiple times. Precisely the number of tabs that are open. This lead me to believe that the problem was with javascript doing a late binding on the setTimeout
callback, hence I changed it to the following inspired by this:
((tab_element) => {return () => {
window.parent.postMessage(tab_element.document.querySelectorAll("h1.search-name.section-title.search-title-searchname")[0].innerText);
}})(newTabs[newTabs.length - 1])
This actually performs a closure of the actual DOM element I want. But it still does not work.
What am I doing wrong ?
Let me know if any other info is required.
Thanks.
The setTimeout is not called synchronously but after the tab's "load" event fires, by which time the forEach
loop has finished running and left newTabs.length - 1
referring to the last tab.
One solution may be to use a variable to replace repeated use of length-1
. Untested but along the lines of:
alert_list.forEach((currentValue, currentIndex) => {
alert_status = currentValue.childNodes[13].innerText;
if(alert_status == "Enabled") {
// console.log(currentValue.childNodes[3].innerText + " " + currentValue.childNodes[7].innerText);
let tabWin = window.open(currentValue.childNodes[5].children[0].href, "_blank")
newTabs.push( tabWin);
tabWin.onload = function(event) {
setTimeout( () => {
window.parent.postMessage(
tabWin.document.querySelectorAll("h1.search-name.section-title.search-title-searchname")
[0].innerText
);
}, 120*1000);
};
}
});
Here's the original code indented to more clearly show where the delayed evaluation is occurring (with a declaration added for alert_status
:
alert_list.forEach((currentValue, currentIndex) => {
let alert_status = currentValue.childNodes[13].innerText;
if(alert_status == "Enabled") {
console.log(currentValue.childNodes[3].innerText + " " + currentValue.childNodes[7].innerText);
newTabs.push(window.open(currentValue.childNodes[5].children[0].href, "_blank"));
newTabs[newTabs.length - 1].onload = function() {
setTimeout(
(
(tab_element) => {
return () => {
window.parent.postMessage(tab_element.document.querySelectorAll("h1.search-name.section-title.search-title-searchname")[0].innerText);
}
}
)(newTabs[newTabs.length - 1])
, 120*1000);
};
}
});
The anonymous onload function is compiled into a function object when it is added but not executed until the load event fires. When it does execute, it creates a "closure" for tab_element
taking it's value from newTabs[newTabs.length - 1]
which is now the last tab element.
The solution is to remove the trick code that is causing the problem.
When a function is called, a record (an object in JavaScript terms) is used to hold the values of variables and functions used within the function. In effect, variable identifiers are now bound to such an "environment record". In ES3 and earlier no additional records were required at the block level (of statements within curly braces), or for the special case of a for( let identifier...
loop because let
, const
and class
declarations had yet to be introduced. The single environment record in ES3 was called the "activation object".
Normally when a function returns, the environment records created for an individual call could be garbage collected on the basis they could no longer be reached through code - the current criterion for JavaScript memory garbage collection (MGC).
If and while nested functions and variables values can be reached in code after the function exits however, they aren't eligible to be removed from memory. This situation is typically described as the functions and variables being held in a "closure".
winVar
in the example solution is held in a different environment record for each call forEach
makes to its function argument - the different values generated in successive calls don't overwrite each other.setTimeout
call back functions are different function objects in separate closures. The setTimeout
call back has access to environment record(s) of its parent onload
function, which has access to environment record(s) of its parent tab-opening function.tabWin
reference in the timer callback ultimately resolves to the binding of the identifier held in an environment record of the forEach
function argument which opened the tab window when called.