使用 MutationObserver 检测输入值变化

Detect input value change with MutationObserver

我想检测 text/value 输入字段何时发生变化。即使我用 js 更改值,我也想检测到该更改。

这是我目前在 demo in fiddle 中尝试过的内容。

HTML:

<input type="text" id="exNumber"/>

JavaScript:

var observer = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    // console.log('Mutation type: ' + mutation.type);
    if ( mutation.type == 'childList' ) {
      if (mutation.addedNodes.length >= 1) {
        if (mutation.addedNodes[0].nodeName != '#text') {
           // console.log('Added ' + mutation.addedNodes[0].tagName + ' tag.');
        }
      }
      else if (mutation.removedNodes.length >= 1) {
         // console.log('Removed ' + mutation.removedNodes[0].tagName + ' tag.')
      }
    }
     if (mutation.type == 'attributes') {
      console.log('Modified ' + mutation.attributeName + ' attribute.')
    }
  });   
});

var observerConfig = {
        attributes: true,
        childList: false,
        characterData: false
};

// Listen to all changes to body and child nodes
var targetNode = document.getElementById("exNumber");
observer.observe(targetNode, observerConfig);

要了解发生了什么,必须弄清楚 attribute(内容属性)和 属性 之间的区别(IDL 属性)。我不会对此进行扩展,因为已经有涵盖该主题的优秀答案:

  • Properties and Attributes in HTML
  • .prop() vs .attr()
  • What is happening behind .setAttribute vs .attribute=?

当您通过键入或 JS 更改 input 元素的内容时:

targetNode.value="foo";

浏览器更新 value 属性 但不更新 value 属性 (这反映了 defaultValue 属性。

然后,如果我们查看 spec of MutationObserver,我们将看到 attributes 是可以使用的对象成员之一。因此,如果您显式设置 value 属性:

targetNode.setAttribute("value", "foo");

MutationObserver 将通知属性修改。但是规范列表中没有像properties这样的东西:value属性无法观察到.

如果您想检测用户何时更改您的输入元素的内容,input event is the most straightforward way. If you need to catch JS modifications, go for setInterval 并将新值与旧值进行比较。

检查此 SO question 以了解不同的替代方案及其局限性。

the value property can be observed, Don't waste your time.

function changeValue (event, target) {
    document.querySelector("#" + target).value = new Date().getTime();
}
 
function changeContentValue () {
    document.querySelector("#content").value = new Date().getTime();
}
 
Object.defineProperty(document.querySelector("#content"), "value", {
    set:  function (t) {
        alert('#changed content value');
        var caller = arguments.callee
            ? (arguments.callee.caller ? arguments.callee.caller : arguments.callee)
            : ''
 
        console.log('this =>', this);
        console.log('event => ', event || window.event);
        console.log('caller => ', caller);
        return this.textContent = t;
    }
});
<form id="form" name="form" action="test.php" method="post">
        <input id="writer" type="text" name="writer" value="" placeholder="writer" /> <br />
        <textarea id="content" name="content" placeholder="content" ></textarea> <br />
        <button type="button" >Submit (no action)</button>
</form>
<button type="button" onClick="changeValue(this, 'content')">Change Content</button>

这有效并保留并链接了原始 setter 和 getter,因此您的领域的其他所有内容仍然有效。

var registered = [];
var setDetectChangeHandler = function(field) {
  if (!registered.includes(field)) {
    var superProps = Object.getPrototypeOf(field);
    var superSet = Object.getOwnPropertyDescriptor(superProps, "value").set;
    var superGet = Object.getOwnPropertyDescriptor(superProps, "value").get;
    var newProps = {
      get: function() {
        return superGet.apply(this, arguments);
      },
      set: function (t) {
        var _this = this;
        setTimeout( function() { _this.dispatchEvent(new Event("change")); }, 50);
        return superSet.apply(this, arguments);
      }
    };
    Object.defineProperty(field, "value", newProps);
    registered.push(field);
  }
}

我修改了 一点,想分享一下。无法相信实际上有解决方案。

在输入框中键入以查看默认行为。现在,打开 DevTools 和 select 输入元素,然后更改其值,例如[=12=].value = "hello"。检查 UI 与 API 的区别。似乎 UI 交互不直接修改 value 属性。如果是,它还会记录 "...changed via API...".

let inputBox = document.querySelector("#inputBox");

inputBox.addEventListener("input", function () {
    console.log("Input value changed via UI. New value: '%s'", this.value);
});

observeElement(inputBox, "value", function (oldValue, newValue) {
    console.log("Input value changed via API. Value changed from '%s' to '%s'", oldValue, newValue);
});

function observeElement(element, property, callback, delay = 0) {
    let elementPrototype = Object.getPrototypeOf(element);
    if (elementPrototype.hasOwnProperty(property)) {
        let descriptor = Object.getOwnPropertyDescriptor(elementPrototype, property);
        Object.defineProperty(element, property, {
            get: function() {
                return descriptor.get.apply(this, arguments);
            },
            set: function () {
                let oldValue = this[property];
                descriptor.set.apply(this, arguments);
                let newValue = this[property];
                if (typeof callback == "function") {
                    setTimeout(callback.bind(this, oldValue, newValue), delay);
                }
                return newValue;
            }
        });
    }
}
<input type="text" id="inputBox" placeholder="Enter something" />