上周例会给组内做了个小分享,双向数据绑定
1.发布订阅模式实现
发布订阅模式定义了一种一对多的关系。对于这么多个dom节点绑定同一model特别使用
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>双向数据绑定--订阅-发布者模式</title> </head> <body> <input type="text" data-bind-123="name"/> <p data-bind-123></p> </body> <script> /** * 双线数据绑定--订阅发布者模式实现 */ function addEvent(el,eventName,handle){ if(el.addEventListener){//w3c el.addEventListener(eventName,handle,false); }else if(el.attachEvent){//ie el.attachEvent('on'+eventName,handle); }else{ el['on'+eventName] = handle; } } class DataBind { constructor(id) { this.id = id; this.pubSub = { callbacks:{}, on:function(msg,cb){ this.callbacks[msg] = this.callbacks[msg] || []; this.callbacks[msg].push(cb); }, publish:function(msg){ console.log(this.callbacks[msg]); this.callbacks[msg].forEach((cb) => { cb.apply(null,arguments);//?? }); } }; var message = this.id+':change', dataAttr = 'data-bind-'+this.id; var handleChange = (evt) => { var evt = evt || window.event, target = evt.target || evt.srcElement, prop = target.getAttribute(dataAttr); console.log('input'); if(prop){ this.pubSub.publish(message,prop,target.value); } }; addEvent(document,'input',handleChange); this.pubSub.on(message,(evt,prop,val) => { var els = document.querySelectorAll('['+dataAttr+']'), typeArr = ['TEXTAREA','INPUT','SELECTOR']; console.log(els); for(var i=0,l=els.length;i<l;i++){ if(typeArr.includes(els[i].tagName)){ els[i].value = val; }else{ els[i].innerHTML = val; } } }); } } class Model { constructor(id){ this.id = id; this.attrs = {}; this.dataBind = new DataBind(this.id); } set(attrName,val){ this.attrs[attrName] = val; this.dataBind.pubSub.publish(this.id+':change',attrName,val); } } var user = new Model(123); user.set('name','liubeijing'); </script> </html>
2.数据劫持
vue中的双向数据绑定就是用的数据劫持的方法
Object.defineProperty(obj,{ get:fucntion(){}, set:function(){} })
这个方法不但可以对对象的属性进行拦截,还可以对方法进行拦截
不过我这里用的是es6的最新特性proxy,原理基本相通
class DataBind{ constructor(config){ this._bindings = {}; this.$data = this.watcher(config.data); this.parseArr(this.$data); this.compile(document.getElementsByTagName('body')[0]); } watcher(data){ // 数据拦截,在set里面粗发订阅的事件 var _this = this; return new Proxy(data, { get: function(target, property){ return target.hasOwnProperty(property) && target[property]; }, set: function(target, key, value, receiver){ target[key] = value; if(data.hasOwnProperty(key) && _this._bindings[key]){ for(var i=0,l=_this._bindings[key].length;i<l;i++){ // 发布 _this._bindings[key][i].fn(key ,value); } return true; // 必须要有返回值,否则会报错 } return false; } }); } parseArr(data){ // 解决用数组方法操作数组时不会发生数据拦截的问题 var arrProto = Object.create(Array.prototype); var _this = this; ['shift','unshift','push','pop','slice','splice'].forEach(function(method){ Object.defineProperty(arrProto, method,{ value: function(){ var result = Array.prototype[method].apply(this, arguments); console.log(this.name, arguments); var key = this.name; _this.$data[key] = this; return result; } }) }) for(let i in data){ console.log(typeof data[i]) if(Object.prototype.toString.call(data[i]) === '[object Array]'){ data[i].__proto__ = arrProto; // 挂在实例上,不污染全局 data[i].name = i; // 记住键名 在数组方法里面会用到 } if(Object.prototype.toString.call(data[i]) === '[object Object]'){ this.parseArr(data[i]); } } } compile(root){ // 循环递归 ,查找带有标识的元素,依次给他们订阅时间 var nodes = root.children; var _this = this; if(nodes){ for(var i=0,l=nodes.length;i<l;i++){ var node = nodes[i]; if(!node.children.length){// 没有子元素 let attr = node.getAttribute('v-bind') || node.getAttribute('v-model'); // !!! let isForm = ['INPUT','TEXTAREA', 'SELECT'].indexOf(node.nodeName)>-1; if(attr){ this._bindings[attr] = this._bindings[attr] || []; if(isForm){ node.value = _this.$data[attr]; // 回显数据 node.oninput = function(){ _this.$data[attr] = this.value; }; }else{ node.innerHTML = _this.$data[attr]; // 回显数据 } this._bindings[attr].push({ // 订阅 el: node, fn: function(key, value){ if(isForm){ this.el.value = value; }else{ this.el.innerHTML = value; } } }); } }else{ this.compile(node); } } } } }
光用数据拦截Object.defineProperty,会造成一个问题,就是用数组里面的方法对数组进行操作时不会触发拦截
parseArr(data){ // 解决用数组方法操作数组时不会发生数据拦截的问题 var arrProto = Object.create(Array.prototype); var _this = this; ['shift','unshift','push','pop','slice','splice'].forEach(function(method){ Object.defineProperty(arrProto, method,{ value: function(){ var result = Array.prototype[method].apply(this, arguments); console.log(this.name, arguments); var key = this.name; _this.$data[key] = this; return result; } }) }) for(let i in data){ console.log(typeof data[i]) if(Object.prototype.toString.call(data[i]) === '[object Array]'){ data[i].__proto__ = arrProto; // 挂在实例上,不污染全局 data[i].name = i; // 记住键名 在数组方法里面会用到 } if(Object.prototype.toString.call(data[i]) === '[object Object]'){ this.parseArr(data[i]); } } }
上面的方法可以轻松解决这个问题,最后这里面有个问题,就是model不支持多级调用,如
var data = { name: 'hahah', obj: { phone } } data.name //会发生拦截 data.obj.phone //不会发生拦截
3.脏检测
给每个model添加一份拷贝,每次数据发生变化时,会一次和拷贝里面的值进行比较,如果不一样,这更新视图,同时要更新拷贝里面的值
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>脏检测</title> </head> <body> <input type="text" v-model="name"> <p v-bind="name"></p> </body> <script> let data = { name: 'liubeijing' }; let watchList = []; function digest(){ for(let key in data){ let oldVal = watchList[key].last; let newVal = watchList[key].get(); if(oldVal !== newVal){ for(let name in watchList[key].callback){ watchList[key].callback[name](); } } } } let root = document.getElementsByTagName('body')[0]; function compile(nodes){ for(let i=0,l=nodes.length;i<l;i++){ let node = nodes[i]; console.log(node); if(!node) continue; if(node.children.length){ compile(node.children); }else{ let attr = node.getAttribute('v-bind') || node.getAttribute('v-model'); // !!! function domUpdate(){ if(['INPUT','TEXTAREA', 'SELECT'].indexOf(node.nodeName)>-1){ node.value = data[attr]; // 回显数据 }else{ node.innerHTML = data[attr]; // 回显数据 } } if(attr){ domUpdate(); watchList[attr] = watchList[attr] || { last: data[attr], get: function(){ return data[attr]; }, callback: [] }; watchList[attr].callback.push(function(){ domUpdate(); watchList[attr].last = data[attr]; // 更新旧值 }); } } } } compile(root.children); document.getElementsByTagName('input')[0].oninput = function(evt){ evt = window.event || evt; let attr = this.getAttribute('v-model'); data[attr] = evt.target.value; console.log(evt.target.value); digest(); } </script> </html>