【问题标题】:Are lapsed listeners preventable in javascript?在 JavaScript 中是否可以预防失效的侦听器?
【发布时间】:2017-05-03 11:03:24
【问题描述】:

我的问题真的是“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。有什么办法吗?

【问题讨论】:

  • “从概念上讲,DOM 节点应该拥有监听器”——那你为什么要持有一个监听器数组呢?听起来您没有单一所有权。
  • @the8472 我很难弄清楚如何回答您的问题。您是在问为什么 observable 在我的小提琴中保留了一系列听众?当然,这是为了记住当值发生变化时要调用谁。如果我可以按照推荐的方式编写代码,那么该侦听器数组将替换为侦听器的弱引用数组,这将构成所有权,并且我'd 还将在 DOM 节点中存储对侦听器的引用,以便 DOM 节点将成为相应侦听器的单一所有者。你知道什么是弱引用,对吧?
  • 是的,但是如果没有弱引用,该数组实际上会导致多重所有权。所以你要么需要想办法手动切断这些引用(例如突变观察者),要么首先避免它们的存在。
  • @the8472 你已经跳到“没有弱引用”了。没有人说明是否有可能实际上使它们成为弱引用。如果可能的话,这将是首选。回复:“首先避免他们的 [the array of listeners'] 存在”,如果您知道如何实现观察者模式,而无需被观察者保留对听众的引用列表,请解释一下。回复:突变观察者,如果你知道如何让突变观察者在给定节点从 DOM 根的可达性发生变化时通知我,请解释并链接到示例代码。
  • mutation 观察者只是一个例子(但你可以找到很多教程和涵盖它们的 SO 问题),将你的复选框(作为视图)包装到模型类中并通过它控制它们的删除是另一种选择手动切断这些边缘。懒惰清理是另一种选择。您的问题远非无法解决,但首选方法将取决于实际应用中的情况。对于 dom 节点,特别是根本不保留引用,而是使用选择器也可以工作

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


【解决方案1】:

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 元素 &lt;my-control&gt;

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

这里的关键点是,当&lt;my-control&gt; 从DOM 中移除时,无论出于何种原因,都保证会调用disconnectedCallback()。我们用它来移除监听器。

您现在可以添加第一个 &lt;my-control&gt;

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

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

【讨论】:

    【解决方案2】:

    你可以使用mutation observers哪个

    提供监视对 DOM 树所做更改的能力。它旨在替代旧的 Mutation Events 功能,该功能是 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]
      })
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-01-14
      相关资源
      最近更新 更多