上周例会给组内做了个小分享,双向数据绑定

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>