0

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:

  1. Prepare postMessage() listener on main window:

    window.addEventListener('message', (event) => { console.log(event.data); }, false);

  2. Create my tabs array and get my DOM elements:

    alert_list = document.querySelectorAll("tr.expand.alerts-table-tablerow"); var newTabs = [];

  3. 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.

ng.newbie
  • 2,807
  • 3
  • 23
  • 57

1 Answers1

1

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.


How closures work in JavaScript is topic unto itself. An introduction for the purposes of explaining this answer however:
  • 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".

    1. The value of 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.
    2. The onload and 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.
    3. The 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.
traktor
  • 17,588
  • 4
  • 32
  • 53
  • Nopes, I dont understand. Please note that I am supplying setTimeout with a callback function immediately, and that function contains a closure to the actual window element. `((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 executes immediately and does not wait for `onload`, the function that is returned by this function executes after the timeout/onload. – ng.newbie Mar 28 '20 at 12:33
  • Do you agree with my analysis ? Am I going wrong somewhere ? – ng.newbie Mar 28 '20 at 12:34
  • No, I disagree - nothing inside the anonymous function used to set `.onload` is executed until the "load" event fires, and it is only then that the final `newTabs[newTabs.length - 1]` is evaluated. I'll expand the answer to explain. – traktor Mar 28 '20 at 13:30
  • Correct me if I am wrong, but the whole reason your solution works is because `let` allows you to declare variables within its own lexical scope instead of hoisting them like `var`. So when you are referring to the `tabWin` inside `setTimeout` you are actually referring to the object created in that iteration. – ng.newbie Mar 29 '20 at 09:13
  • Not exactly. `tabWin` can be seen by the timeout function because it (the timeout function) is nested within the function that declared `tabWin` (the function called by `forEach`). While usage is a closure (the outer function has returned), `tabWin` could have been declared using `var` without making any difference. See https://stackoverflow.com/q/750486/5217142 for more discussion. – traktor Mar 29 '20 at 09:47
  • Ok here is what I don't understand, the definition of a closure is "Closure means that an inner function always has access to the vars and parameters of its outer function, even after the outer function has returned.". Now the value of `tabWin` will keep on changing, so when `setTimeout` tries to access it it will get the last value. **Unless, we can think of closures as snapshots**. Meaning the `tabWin` referenced in `setTimeout` has no relation with the variable in the main window. Am I on the right track ? – ng.newbie Mar 29 '20 at 10:26
  • Comments are not actually intended for prolonged discussion. I have updated the answer in response, but if you require more details please research further or ask a new question, thanks. – traktor Apr 01 '20 at 00:49