通常來說,如果我們自己通過 value 改變了 input 元素的值,我們肯定是知道的,但是在某些場景下,頁面上有別的邏輯在改變 input 的 value 值,我們可能希望能在這個值發生變化的時候收到通知。於是我們想到了 onchange 事件,然而我們遺憾的發現,onchange 事件卻並不會被觸發,因為onchange事件觸發是有條件的。
onchange 事件的觸發條件
onchange 觸發需要三個步驟:
- input 元素獲得焦點
- input 元素的值發生變化
- input 元素失去焦點
而且必須是點擊觸發的,這句話的意思是,盡管我們可以通過 input.focus() 使 input 元素獲得焦點,可以通過 input.value 改變值,可以通過 input.blur() 使元素失去焦點,但是這並不會觸發 onchange 事件,可以看下面的 demo 一探究竟:
如何在改變 value 時獲得通知
一種方法是使用 timer。通過 setInterval 的方式來不斷查看 value 值是否發生變化。這種方法雖然可以 work,但是實時性不是很好,也比較浪費資源。所以有沒有第二種方法呢,答案是本文接下來要說的 -- 重寫 value 屬性 。
其實這種操作盡管不推薦,但是還是比較常見的。比如 Vue,通過重寫 Array 的 push,pop,concat 等方法,從而實現了只要對數組進行上述操作,就能觸發界面更新。那么接下來,我們來嘗試重寫 input 元素的這個 value 屬性,實現改變 value 值時,我們可以得到通知。
可以判斷的是,value 絕對不是一個簡單的值,所以我們先看看 value 是如何定義的:
let input = document.querySelector(input);
console.log(Object.getOwnPropertyDescriptor(input, 'value'))
可以看到打印出來是 undefined,所以 value 這個屬性是 input 元素繼承過來的,也就是位於 HTMLInputElement 的 prototype 上 -- input.constructor.prototype 或者 input.__proto__。於是將上面的代碼改一下:
console.log(Object.getOwnPropertyDescriptor(input.__proto__, 'value'))
打印結果如下:

於是我們知道了 value 是掛在 input 元素原型對象上的一個 getter 和 setter 的屬性。那么接下來,我們只要改寫 setter,在 setter 中加入通知代碼,然后同時調用原來的 setter,就可以檢測 value 的變化。代碼如下:
let descriper = Object.getOwnPropertyDescriptor(input.__proto__, 'value');
// 取出原先的 get 和 set 函數
let getValue = descriper.get;
let setValue = descriper.set;
Object.defineProperty(
input.__proto__,
'value',
{
configurable: true,
enumerable: true,
get: function (){
return getValue.call(this);
},
// 重寫 set 方法
set: function (){
console.log(arguments, this);
// 加入通知代碼
$(this).trigger('valChange');
setValue.call(this, ...arguments);
}
})
下面是一個 demo,可以看到,點擊 button 設置 value 時,可以被看到控制台打印出 value 發生變化。
總結
在通過 js 設置 value 時,無法觸發 onchange 事件,這里這個問題提供了另外一種解決思路,基本思想上寫一個新的函數替換原有 value 屬性的 setter,在新函數中加入自己的邏輯后調用原有的 setter。(本文完)
