Are listeners skipped in javascript?

My question is really " expired listener issue preventable in javascript?" but apparently the word "problem" is causing the problem.

On a wikipedia page, a claimed issue with a listener can be resolved by an entity holding weak links to observers. I implemented this before in Java, and it works well, and I thought I would implement it in Javascript, but now I don’t understand how to do it. Does javascript even have weak links? I see that there is WeakSet, and WeakMapwho have a "weak" in their names, but they seem to me not useful, as far as I can see.

Here's a jsfiddle showing a typical case of a problem.

html:

<div id="theCurrentValueDiv">current value: false</div>
<button id="thePlusButton">+</button>

javascript:

'use strict';
console.log("starting");
let createListenableValue = function(initialValue) {
  let value = initialValue;
  let listeners = [];
  return {
    // Get the current value.
    get: function() {
      return value;
    },
    // Set the value to newValue, and call listener()
    // for each listener that has been added using addListener().
    set: function(newValue) {
      value = newValue;
      for (let listener of listeners) {
        listener();
      }
    },
    // Add a listener that set(newValue) will call with no args
    // after setting value to newValue.
    addListener: function(listener) {
      listeners.push(listener);
      console.log("and now there "+(listeners.length==1?"is":"are")+" "+listeners.length+" listener"+(listeners.length===1?"":"s"));
    },
  };
};  // createListenable

let theListenableValue = createListenableValue(false);

theListenableValue.addListener(function() {
  console.log("    label got value change to "+theListenableValue.get());
  document.getElementById("theCurrentValueDiv").innerHTML = "current value: "+theListenableValue.get();
});

let nextControllerId = 0;

let thePlusButton = document.getElementById("thePlusButton");
thePlusButton.addEventListener('click', function() {
  let thisControllerId = nextControllerId++;
  let anotherDiv = document.createElement('div');
  anotherDiv.innerHTML = '<button>x</button><input type="checkbox"> controller '+thisControllerId;
  let [xButton, valueCheckbox] = anotherDiv.children;
  valueCheckbox.checked = theListenableValue.get();
  valueCheckbox.addEventListener('change', function() {
    theListenableValue.set(valueCheckbox.checked);
  });

  theListenableValue.addListener(function() {
    console.log("    controller "+thisControllerId+" got value change to "+theListenableValue.get());
    valueCheckbox.checked = theListenableValue.get();
  });

  xButton.addEventListener('click', function() {
    anotherDiv.parentNode.removeChild(anotherDiv);
    // Oh no! Our listener on theListenableValue has now lapsed;
    // it will keep getting called and updating the checkbox that is no longer
    // in the DOM, and it will keep the checkbox object from ever being GCed.
  });

  document.body.insertBefore(anotherDiv, thePlusButton);
});

In this script, the observed state is a logical value, and you can add and remove flags that view and control it, all synchronized by listeners on it. The problem is that when you delete one of the controllers, its listener does not leave: the listener continues to receive calls and update the checkbox and does not allow the checkbox to be GCed, even if the checkbox is no longer in the DOM and otherwise GCable. This can be seen in the javascript console, as the listener callback displays a message to the console.

, DOM node GCable, node DOM. , DOM node , . ?

, , x, DOM, , - DOM, node, document.body.innerHTML = ''. , , , DOM , , GCable. ?

+5
2

Custom_elements . Chrome Safari ( 2018 ) Firefox Edge.

jsfiddle HTML:

<div id="theCurrentValue">current value: false</div>
<button id="thePlusButton">+</button>

listenableValue, :

"use strict";
function createListenableValue(initialValue) {
    let value = initialValue;
    const listeners = [];
    return {
        get() { // Get the current value.
            return value;
        },
        set(newValue) { // Set the value to newValue, and call all listeners.
            value = newValue;
            for (const listener of listeners) {
                listener();
            }
        },
        addListener(listener) { // Add a listener function to  call on set()
            listeners.push(listener);
            console.log("add: listener count now:  " + listeners.length);
            return () => { // Function to undo the addListener
                const index = listeners.indexOf(listener);
                if (index !== -1) {
                    listeners.splice(index, 1);
                }
                console.log("remove: listener count now:  " + listeners.length);
            };
        }
    };
};
const listenableValue = createListenableValue(false);
listenableValue.addListener(() => {
    console.log("label got value change to " + listenableValue.get());
    document.getElementById("theCurrentValue").innerHTML
        = "current value: " + listenableValue.get();
});
let nextControllerId = 0;

HTML <my-control>:

customElements.define("my-control", class extends HTMLElement {
    constructor() {
        super();
    }
    connectedCallback() {
        const n = nextControllerId++;
        console.log("Custom element " + n + " added to page.");
        this.innerHTML =
            "<button>x</button><input type=\"checkbox\"> controller "
            + n;
        this.style.display = "block";
        const [xButton, valueCheckbox] = this.children;
        xButton.addEventListener("click", () => {
            this.parentNode.removeChild(this);
        });
        valueCheckbox.checked = listenableValue.get();
        valueCheckbox.addEventListener("change", () => {
            listenableValue.set(valueCheckbox.checked);
        });
        this._removeListener = listenableValue.addListener(() => {
            console.log("controller " + n + " got value change to "
                + listenableValue.get());
            valueCheckbox.checked = listenableValue.get();
        });
    }
    disconnectedCallback() {
        console.log("Custom element removed from page.");
        this._removeListener();
    }
});

, disconnectedCallback() , <my-control> DOM . , .

<my-control> :

const plusButton = document.getElementById("thePlusButton");
plusButton.addEventListener("click", () => {
    const myControl = document.createElement("my-control");
    document.body.insertBefore(myControl, plusButton);
});

( , , , .)

0

,

, DOM. Mutation Events, DOM3 Events.

, ,

if (window && window.MutationObserver) {
  var observer = new MutationObserver(function (mutations) {
    if (Object.keys(watch).length < 1) return
    for (var i = 0; i < mutations.length; i++) {
      if (mutations[i].attributeName === KEY_ATTR) {
        eachAttr(mutations[i], turnon, turnoff)
        continue
      }
      eachMutation(mutations[i].removedNodes, function (index, el) {
        if (!document.documentElement.contains(el)) turnoff(index, el)
      })
      eachMutation(mutations[i].addedNodes, function (index, el) {
        if (document.documentElement.contains(el)) turnon(index, el)
      })
    }
  })

  observer.observe(document.documentElement, {
    childList: true,
    subtree: true,
    attributes: true,
    attributeOldValue: true,
    attributeFilter: [KEY_ATTR]
  })
}
0

Source: https://habr.com/ru/post/1676245/


All Articles