如何监听 js 中变量的变化? 我现在有这样一个需求,需要监控js的某个变量的改变,如果该变量发生变化,则触发一些事件,不能使用timeinterval之类的定时去监控的方法,不知道有比较好的解决方案么?
这个问题问的很好。
流行的MVVM的JS库/框架都有共同的特点就是数据绑定,在数据变更后响应式的自动进行相关计算并变更DOM展现。所以这个问题也可以理解为如何实现MVVM库/框架的数据绑定 。
常见的数据绑定的实现有脏值检测,基于ES5的getter
和setter
,以及ES已被废弃的Object.observe
,和ES6中添加的Proxy
。
脏值检测 angular使用的就是脏值检测,原理是比较新值和旧值,当值真的发生改变时再去更改DOM,所以angular中有一个$digest
。那么为什么在像ng-click
这样的内置指令在触发后会自动变更呢?原理也很简单,在ng-click
这样的内置指令中最后追加了$digest
。
简易的实现一个脏值检测:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 <!DOCTYPE html > <html > <head > <meta charset ="utf-8" /> <title > two-way binding</title > </head > <body onload ="init()" > <button ng-click ="inc" > Increase </button > <button ng-click ="reset" > Reset </button > <span style ="color:red" ng-bind ="counter" > </span > <span style ="color:blue" ng-bind ="counter" > </span > <span style ="color:green" ng-bind ="counter" > </span > <script type ="text/javascript" > var counter = 0 ; function inc () { counter++; } function reset () { counter = 0; } function init () { bind(); } function bind () { var list = document .querySelectorAll("[ng-click]" ); for (var i=0 ; i<list.length; i++) { list[i].onclick = (function (index) { return function () { window [list[index].getAttribute("ng-click" )](); apply(); }; })(i); } } function apply () { var list = document .querySelectorAll("[ng-bind='counter']" ); for (var i=0 ; i<list.length; i++) { if (list[i].innerHTML != counter) { list[i].innerHTML = counter; } } } </script > </body > </html >
这样做的坏处是自己变更数据后,是无法自动改变DOM的,必须要想办法触发apply()
,所以只能借助ng-click
的包装,在ng-click
中包含真实的click
事件监听并追加脏值检测以判断是否要更新DOM。
另外一个坏处是如果不注意,每次脏值检测会检测大量的数据,而很多数据是没有检测的必要的,容易影响性能。
关于如何实现一个和angular一样的脏值检测,知道原理后还有很多工作要去做,以及如何优化等等。如果有兴趣可以看看民工叔曾经推荐的《Build Your Own Angular.js》,第一章Scope
便讲了如何实现angular的作用域和脏值检测。对了,上面的例子也是从民工叔的博客稍加修改来的,建议最后去看下原文,链接在参考资料中。
ES5的getter
与setter
在ES5中新增了一个Object.defineProperty
,直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。
1 Object .defineProperty(obj, prop, descriptor)
其接受的第三个参数可以取get
和set
并各自对应一个getter
和setter
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 ; console .log(a.zhihu);
基于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直接修改了数组的原型方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 const arrayProto = Array .prototypeexport const arrayMethods = Object .create(arrayProto);[ 'push' , 'pop' , 'shift' , 'unshift' , 'splice' , 'sort' , 'reverse' ] .forEach(function (method ) { var original = arrayProto[method] def(arrayMethods, method, function mutator ( ) { 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) ob.dep.notify() return result }) })
这样重写了原型方法,在执行数组变异方法后依然能够触发视图的更新。
但是这样还是不能解决修改数组的length
和直接用索引设置元素如items[0] = {}
的问题,想要解决依然可以参考Vue的做法: 前一个问题可以直接用新的数组代替旧的数组;后一个问题可以为数组拓展一个$set
方法,在执行修改后顺便触发视图的更新。
已被废弃的Object.observe
Object.observe
曾在ES7的草案中,并在提议中进展到stage2,最终依然被废弃。 这里只举一个MDN上的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var user = { id: 0 , name: 'Brendan Eich' , title: 'Mr.' }; function updateGreeting ( ) { user.greeting = 'Hello, ' + user.title + ' ' + user.name + '!' ; } updateGreeting(); Object .observe(user, function (changes ) { changes.forEach(function (change ) { if (change.name === 'name' || change.name === 'title' ) { updateGreeting(); } }); });
由于是已经废弃了的特性,Chrome虽然曾经支持但也已经废弃了支持,这里不再讲更多,有兴趣可以搜一搜以前的文章,这曾经是一个被看好的特性(Object.observe()带来的数据绑定变革 )。 当然关于它也有一些替代品Polymer/observe-js 。
ES6带来的Proxy
人如其名,类似HTTP中的代理:
1 var p = new Proxy (target, handler);
target
为目标对象,可以是任意类型的对象,比如数组,函数,甚至是另外一个代理对象。handler
为处理器对象,包含了一组代理方法,分别控制所生成代理对象的各种行为。
举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 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。
参考资料