在javascript中可以阻止已失效的侦听器吗?

时间:2017-05-03 11:03:25

标签: javascript dom garbage-collection observer-pattern weak-references

我的问题确实是" the lapsed listener problem在javascript中是否可以预防?"但显然是"问题"导致问题。

维基百科页面说,失效的监听器问题可以通过持有weak references给观察者的主题来解决。我之前在Java中已经实现了它并且运行良好,并且我认为我在Javascript中实现它,但现在我不知道如何实现它。 javascript甚至有弱引用吗?我看到WeakSetWeakMap有"弱"以他们的名义,但就我所见,他们似乎对此没有帮助。

这里有一个显示问题典型案例的jsfiddle

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);
});

在这个小提琴中,可观察状态是一个布尔值,您可以添加和删除查看和控制它的复选框,所有这些复选框都由侦听器保持同步。 问题是当你删除其中一个控制器时,它的监听器不会消失:监听器不断被调用并更新控制器复选框并阻止复选框被GCed,即使复选框不再在DOM中并且是否可以GCable。您可以在javascript控制台中看到这种情况,因为侦听器回调会将消息输出到控制台。

当我从DOM中删除节点时,我想要的是控制器DOM节点及其关联的值侦听器变为GCable。从概念上讲,DOM节点应该拥有侦听器,而observable应该拥有对侦听器的弱引用。有没有一种干净的方法来实现这一目标?

我知道我可以通过使x按钮显式删除侦听器以及DOM子树来解决问题,但在应用程序中的某些其他代码的情况下这并没有帮助稍后删除包含我的控制器节点的DOM的一部分,例如执行document.body.innerHTML = ''。我喜欢设置,以便在发生这种情况时,我创建的所有DOM节点和监听器都会被释放并变为GCable。有办法吗?

2 个答案:

答案 0 :(得分:0)

Custom_elementslapsed listener problem提供了解决方案。 Chrome和Safari支持它们,并且(截至2018年8月)很快将在Firefox和Edge中得到支持。

我对HTML做了jsfiddle

<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>都可以确保调用<my-control>。我们使用它来删除侦听器。

您现在可以使用以下命令添加第一个const plusButton = document.getElementById("thePlusButton"); plusButton.addEventListener("click", () => { const myControl = document.createElement("my-control"); document.body.insertBefore(myControl, plusButton); });

def decorator(cls):
    class Wrapped(Base):
        def __init__(self, *args, **kwargs):
            self.wrapped = cls(*args, **kwargs)

     return Wrapped


@decorator
class BaseObj(object):
   pass

(这个答案是我在观看this video时发生的,演讲者解释了自定义元素可能有用的其他原因。)

答案 1 :(得分:0)

您可以使用mutation observers

  

提供了监视对DOM树所做的更改的能力。它旨在替代DOM3事件规范中较早的“突变事件”功能。

如何使用它的示例可以在on-load

的代码中找到
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]
  })
}