如何監聽 js 中變數的變化?
我現在有這樣一個需求,需要監控js的某個變數的改變,如果該變數發生變化,則觸發一些事件,不能使用timeinterval之類的定時去監控的方法,不知道有比較好的解決方案么?
比如我定義了如下全局變數:需要做的是當config.jiankong值被其他js文件中的某些函數改變時,需要觸髮指定的事件!
- var a= { zhihu:0 };
這個問題問的很好。
流行的MVVM的JS庫/框架都有共同的特點就是數據綁定,在數據變更後響應式的自動進行相關計算並變更DOM展現。所以這個問題也可以理解為如何實現MVVM庫/框架的數據綁定。
常見的數據綁定的實現有臟值檢測,基於ES5的getter和setter,以及ES已被廢棄的Object.observe,和ES6中添加的Proxy。
臟值檢測angular使用的就是臟值檢測,原理是比較新值和舊值,當值真的發生改變時再去更改DOM,所以angular中有一個$digest。那麼為什麼在像ng-click這樣的內置指令在觸發後會自動變更呢?原理也很簡單,在ng-click這樣的內置指令中最後追加了$digest。
簡易的實現一個臟值檢測:
&
&
&
&
&
&
&
&
&
&&
&&
&&
&
&
&
這樣做的壞處是自己變更數據後,是無法自動改變DOM的,必須要想辦法觸發apply(),所以只能藉助ng-click的包裝,在ng-click中包含真實的click事件監聽並追加臟值檢測以判斷是否要更新DOM。
另外一個壞處是如果不注意,每次臟值檢測會檢測大量的數據,而很多數據是沒有檢測的必要的,容易影響性能。
關於如何實現一個和angular一樣的臟值檢測,知道原理後還有很多工作要去做,以及如何優化等等。如果有興趣可以看看民工叔曾經推薦的《Build Your Own Angular.js》,第一章Scope便講了如何實現angular的作用域和臟值檢測。對了,上面的例子也是從民工叔的博客稍加修改來的,建議最後去看下原文,鏈接在參考資料中。
ES5的getter與setter在ES5中新增了一個Object.defineProperty,直接在一個對象上定義一個新屬性,或者修改一個已經存在的屬性, 並返回這個對象。
Object.defineProperty(obj, prop, descriptor)
其接受的第三個參數可以取get和set並各自對應一個getter和setter方法:
var a = { zhihu:0 };
Object.defineProperty(a, "zhihu", {
get: function() {
console.log("get:" + zhihu);
return zhihu;
},
set: function(value) {
zhihu = value;
console.log("set:" + zhihu);
}
});
a.zhihu = 2; // set:2
console.log(a.zhihu); // get:2
// 2
基於ES5的getter和setter可以說幾乎完美符合了要求。為什麼要說幾乎呢?
首先IE8及更低版本IE是無法使用的,而且這個特性是沒有polyfill的,無法在不支持的平台實現,這也是基於ES5getter和setter的Vue.js不支持IE8及更低版本IE的原因。也許有人會提到avalon,avalon在低版本IE藉助vbscript一些黑魔法實現了類似的功能。
除此之外,還有一個問題就是修改數組的length,直接用索引設置元素如items[0] = {},以及數組的push等變異方法是無法觸發setter的。
如果想要解決這個問題可以參考Vue的做法,在Vue的observer/array.js中,Vue直接修改了數組的原型方法:const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
/**
* Intercept mutating methods and emit events
*/
;[
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse"
]
.forEach(function (method) {
// cache original method
var original = arrayProto[method]
def(arrayMethods, method, function mutator () {
// avoid leaking arguments:
// http://jsperf.com/closure-with-arguments
var i = arguments.length
var args = new Array(i)
while (i--) {
args[i] = arguments[i]
}
var result = original.apply(this, args)
var ob = this.__ob__
var inserted
switch (method) {
case "push":
inserted = args
break
case "unshift":
inserted = args
break
case "splice":
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
這樣重寫了原型方法,在執行數組變異方法後依然能夠觸發視圖的更新。
但是這樣還是不能解決修改數組的length和直接用索引設置元素如items[0] = {}的問題,想要解決依然可以參考Vue的做法:
前一個問題可以直接用新的數組代替舊的數組;後一個問題可以為數組拓展一個$set方法,在執行修改後順便觸發視圖的更新。
已被廢棄的Object.observeObject.observe曾在ES7的草案中,並在提議中進展到stage2,最終依然被廢棄。
這裡只舉一個MDN上的例子:// 一個數據模型
var user = {
id: 0,
name: "Brendan Eich",
title: "Mr."
};
// 創建用戶的greeting
function updateGreeting() {
user.greeting = "Hello, " + user.title + " " + user.name + "!";
}
updateGreeting();
Object.observe(user, function(changes) {
changes.forEach(function(change) {
// 當name或title屬性改變時, 更新greeting
if (change.name === "name" || change.name === "title") {
updateGreeting();
}
});
});
由於是已經廢棄了的特性,Chrome雖然曾經支持但也已經廢棄了支持,這裡不再講更多,有興趣可以搜一搜以前的文章,這曾經是一個被看好的特性(Object.observe()帶來的數據綁定變革)。
當然關於它也有一些替代品Polymer/observe-js。ES6帶來的Proxy人如其名,類似HTTP中的代理:
var p = new Proxy(target, handler);
target為目標對象,可以是任意類型的對象,比如數組,函數,甚至是另外一個代理對象。
handler為處理器對象,包含了一組代理方法,分別控制所生成代理對象的各種行為。舉個例子:
let a = new Proxy({}, {
set: function(obj, prop, value) {
obj[prop] = value;
if (prop === "zhihu") {
console.log("set " + prop + ": " + obj[prop]);
}
return true;
}
});
a.zhihu = 100;
當然,Proxy的能力遠不止此,還可以實現代理轉發等等。
但是要注意的是目前瀏覽器中只有Firefox18支持這個特性,而babel官方也表明不支持這個特性:
Unsupported feature
Due to the limitations of ES5, Proxies cannot be transpiled or polyfilled.
目前已經有babel插件可以實現,但是據說實現的比較複雜。
如果是Node的話升級到目前的最新版本應該就可以使用了,上面的例子測試環境為Node v6.4.0。參考資料- Angular沉思錄(一)數據綁定
- Object.defineProperty() - JavaScript | MDN
- vue/array.js at dev · vuejs/vue
- Object.observe() - JavaScript | MDN
- Proxy - JavaScript | MDN
1.使用 defineProperty 或 __define_setter__ 給寫屬性添加回調函數。對瀏覽器兼容性有要求。
2.使用現成的庫,比如 watch.js。不過在老版本瀏覽器下 watch.js 使用了定時器監控,此時回調是非同步的,而在新版瀏覽器中回調是同步的,時序不一致。
3.使用 MVVM 框架 avalon,此框架使用 vb script 實現了老版本 IE 的兼容,沒有使用定時器,不過針對你的簡單需求,略有點龐大。
4.數組成員不能靠以上方法監聽,目前我所知的只能間接監聽一部分數組函數,實現方法就是函數替換,替換目標是那些會改變 this 數組的「非純函數」,比如splice。
班門不弄斧,耍耍花拳。
高票答案說的很具體了,其實也就是各種雙向綁定的實現機制。這裡說下knockout的方式,knockout採用的是函數的方式,將變數名作為函數名註冊到監視表離,通過回調函數就可以實現對變數的監控,簡單的來說就是每次調用a(value)的時候都會觸發相應的回調,從而完成對變數的監控。別用變數就好了~弄個函數,裡面聲明一個變數,或者用一個全局變數,需要修改的時候得調用這個函數,這樣你就能知道了。
Object有個watch方法,ES5的屬性,兼容性看這裡
Object.prototype.watch()stackoverflow上有大神寫了object.watch shim ,支持IE8 +Watch for object properties changes in JavaScript可以試試Knockout.js。
前面 @DaraW的回答已經比較詳細,補充一種簡單的方法
var EventEmitter = require("events");
var util = require("util");
function Person(name) {
var name=name;
this.getName=function () {
return name;
}
this.setName=function (newName) {
if(newName!==name){
var oldName=name;
name=newName;
this.emit("valueChange",{
key:"name",
oldVal:oldName,
newVal:newName
})
}
}
}
util.inherits(Person, EventEmitter);
var person=new Person("Zhihu");
person.on("valueChange",function(event){
//doSomeThing
})
EventEmitter和util.inherits的具體實現不一,就不貼源碼了。
這種方法的好處是兼容性較好,變相實現private,壞處當然也很明顯,屬性只能通過閉包去讀寫,讀寫的方法也會在每個對象重複出現,佔用內存。使用O.o(),不過chrome近幾個版本已經去掉了,可以考慮引入observe.js
我寫了一個數據監聽的插件,能滿足以下需求:
- 監聽某個數據對象的變化,最簡化情形,只要有變化就回調。
- 只想監聽數據中的一個或多個部分,回調中返回更多信息,比如改變了哪一個,什麼值。
- 監聽數組格式的數據,不污染數據,支持原生操作。
- 數據處於被監聽狀態,在被頻繁修改的時候不想頻繁地觸發回調,只想處理完再最後一次回調。
- 追加了新的數據,自動追加父級的監聽事件,也是再追加自定義的監聽事件,追加的新數據是另外一個對象,且是已被監聽的對象,能實現監聽事件冒泡。
- 撤銷局部監聽事件
- Github測試地址:https://pageborn.github.io/atom-ui/dest/watch.html項目地址:https://github.com/pageborn/atom-ui大家測試看看
用KnockoutJS可以較容易實現。Knockout的observable可以實現數據的雙向綁定,用被監控的變數名.subscribe(function(){//變數改變後調用該函數})可以實現某個變數變化就調用這個function。
訂閱和發布
Angularjs有一個watch方法可以
可以看angularjs源碼
數組的話可以通過重定義JS系統函數來實現了,通過 Array.prototype.add = func(){ this.push() } 之類的方法 在fun中增加一個自定義的回調事件
推薦閱讀:
※在 Google 搜索 Let it snow 的效果是怎麼實現的?
※好的 Web 前端年薪會有多少?
※從技術角度講,作為一個前端開發人員,怎樣才能寫出讓人眼前一亮的前端頁面?
※關於vertical-align:top問題?
※為什麼說 html 和 css 根本不算編程?
TAG:前端開發 | JavaScript |