Search code examples
javascriptclosurespostmessage

Why does postMessage() only give me info from the last DOM?


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.


Solution

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