随着前端交互复杂度的提高,各种框架如angular,react,vue等也层出不穷,这些框架一个比较重要的技术点就是数据绑定。数据的监听有较多的实现方案,本文将粗略的描述一番,并对其中一个兼容性较好的深刻分析。javascript
目前对象的监听可行的方案:html
脏检查: 须要遍历scope对象树里的$watch数组,使用不当容易形成性能问题前端
ES5 object.defineproperty: 除ie8部分支持 其余基本都彻底支持vue
ES7 object.observe : 已经移除(原因)出ES7草案java
gecko object.watch :目前只有基于gecko的浏览器如火狐支持,官方建议仅供调试用react
ES6 Proxy: 目前支持较差,babel也暂不支持转化git
ES5现代浏览器基本都支持了,OK,本文将介绍目前支持度最好的object.defineproperty 的Setters 和 Getters方式github
它属于es5规范,有两种定义属性:数组
一种是 数据属性 包含Writable,Enumerable,Configurable浏览器
一种是 访问器属性 包含get 和set
数据属性的例子
obj.key='static'; //等效于 Object.defineProperty(obj, "key", { enumerable: true, configurable: true, writable: true, value: "static" });
访问器属性例子
var obj = { temperature:'test' }; var temperature=''; Object.defineProperty(obj, 'temperature', { get: function() { return temperature+'-----after'; }, set: function(value) { temperature = value; } }) obj.temperature='Test'; //Test-----after console.log(obj.temperature);
将须要监听对象/数组 obj和回调函数callback传入构造函数,this.callback = callback 存储回调函数
遍历对象/数组obj,经过Object.defineProperty将属性所有定义一遍。在set函数里面添加callback函数,设置val值。get函数返回val。
判断对应的obj[key]是否为对象,是则进入第二步,不然继续遍历
遍历结束以后判断该对象是否为数组,是则对操做数组函数如push,pop,shift,unshift等进行封装,操做数组前调用callback函数
比较复杂的是数组的封装,结构以下:
新建一个对象newProto,继承Array的原型,并在newProto上面封装push,pop等数组操做方法,再将传入的array对象的原型设置为newProto。
在获取数据变化的同时,定位该变化数据在原始根对象的位置,以数组表示如:
如[ 'a', 'dd', 'ddd' ] 表示对象obj.a.dd.ddd的属性改变
实现:每一个遍历对象属性都经过path.slice(0)的方式复制入参数组path,生成新数组tpath,给tpath数组push对应的对象属性key,最后在执行set的回调函数时候将tpath当参数传入
watch.js
/** * * @param obj 须要监听的对象或数组 * @param callback 当对应属性变化的时候触发的回调函数 * @constructor */ function Watch(obj, callback) { this.callback = callback; //监听_obj对象 判断是否为对象,若是是数组,则对数组对应的原型进行封装 //path表明相应属性在原始对象的位置,以数组表示. 如[ 'a', 'dd', 'ddd' ] 表示对象obj.a.dd.ddd的属性改变 this.observe = function (_obj, path) { var type=Object.prototype.toString.call(_obj); if (type== '[object Object]'||type== '[object Array]') { this.observeObj(_obj, path); if (type == '[object Array]') { this.cloneArray(_obj, path); } } }; //遍历对象obj,设置set,get属性,set属性能触发callback函数,并将val的值改成newVal //遍历结束后再次调用observe函数 判断val是否为对象,若是是则在对val进行遍历设置set,get this.observeObj = function (obj, path) { var t = this; Object.keys(obj).forEach(function (prop) { var val = obj[prop]; var tpath = path.slice(0); tpath.push(prop); Object.defineProperty(obj, prop, { get: function () { return val; }, set: function (newVal) { t.callback(tpath, newVal, val); val = newVal; } }); t.observe(val, tpath); }); }; //经过对特定数组的原型中间放一个newProto原型,该原型继承于Array的原型,可是对push,pop等数组操做属性进行封装 this.cloneArray = function (a_array, path) { var ORP = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; var arrayProto = Array.prototype; var newProto = Object.create(arrayProto); var t = this; ORP.forEach(function (prop) { Object.defineProperty(newProto, prop, { value: function (newVal) { path.push(prop); t.callback(path, newVal); arrayProto[prop].apply(a_array, arguments); }, enumerable: false, configurable: true, writable: true }); }); a_array.__proto__ = newProto; }; //开始监听obj对象,初始path为[] this.observe(obj, []); }
index.html
<body> <ul> <li> <a href="javascript:void(0)" onClick="dataOne()"> 将obj b属性改变 </a> </li> <li> <a href="javascript:void(0)" onClick="dataTwo()"> 将obj a属性的dd属性的ddd属性改变 </a> </li> <li> <a href="javascript:void(0)" onClick="dataThree()"> 将obj a属性的g属性数组第一个值的a属性改变 </a> </li> <li> <a href="javascript:void(0)" onClick="dataFour()"> 将obj a属性的g属性数组push新的值 </a> </li> </ul> <div id="path"> </div> <div id="old-val"> </div> <div id="new-val"> </div> </body> <script src="../src/watch.js"></script> <script> var obj = { a: {e: 4, f: 5, g: [{a: 1, b: 2}, [3, 4]], dd: {ddd: 1}}, b: 2, c: 3 }; new Watch(obj, call); function call(path, newVal, oldVal) { document.getElementById('path').innerHTML='路径:'+path; document.getElementById('old-val').innerHTML='新的值:'+newVal; document.getElementById('new-val').innerHTML='老的值:'+oldVal; } function dataOne() { obj.b = Math.floor(Math.random()*10); } function dataTwo() { obj.a.dd.ddd = Math.floor(Math.random()*10); } function dataThree() { obj.a.g[0].a=Math.floor(Math.random()*10); } function dataFour() { obj.a.g.push(Math.floor(Math.random()*10)); } </script>
具体流程的复杂度基于监听对象的深度,因此下图只对父对象作流程分析
经过定义对象内部属性的setter和getter方法,对将要变化的属性进行拦截代理,在变化前执行预设的回调函数来达到对象监听的目的。
数组则在对象监听以外额外在数组对象上的原型链上加一层原型对象,来拦截掉push,pop等方法,而后在执行预设的回调函数
本文有什么不完善的地方,或者流程图有待改进的地方,敬请斧正。