325行代码搞懂vue双向数据绑定原理

  • 简单vue2双向数据绑定代码,支持解析{{}},v-bind,v-on指令,支持数据监听,mounted钩子回调和methods方法调用
        let iswatch=false;//页面初始加载不执行监听
	function DataBinginit(options) { /*option:主调用函数传回来的参数对象*/
		let self = this;
		this.data = options.data(); /*data对象集合*/
		this.methods = options.methods; /*methods:方法集合*/
		this.watchs = options.watchs; /*watchs:监听对象集合*/
		Object.keys(this.data).forEach(function(key) { /*遍历每一个属性*/
			self.proxyKeys(key); /*创建访问或修改data中的每一个属性的服务*/
		});
		observe(this.data); /*创建记录数据变化的服务*/
		new Compile(options.el, this); //模板编译
		iswatch=true;
		options.mounted.call(this); // 所有事情处理好后执行mounted函数
	}
	DataBinginit.prototype = {
		proxyKeys: function(key) {
			let self = this;
			//Object.defineProperty(obj, prop, descriptor)
			//obj: 需要被操作的目标对象
			//prop: 目标对象需要定义或修改的属性的名称
			//descriptor: 将被定义或修改的属性的描述符
			Object.defineProperty(this, key, {
				enumerable: false, //表示该属性是否可枚举,即是否通过for-in循环或Object.keys()返回属性,如果直接使用字面量定义对象,默认值为true
				configurable: true, //表示能否通过delete删除此属性,能否修改属性的特性,或能否修改把属性修改为访问器属性,如果直接使用字面量定义对象,默认值为true delete:
				/*Writable:false,*/ //当writable为false(并且configrubale为true),[[value]]可以通过defineeProperty修改, 但不能直接赋值修改
				get: function getter() { //一个给属性提供 getter 的方法(访问对象属性时调用的函数,返回值就是当前属性的值),如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined
					return self.data[key];
				},
				set: function setter(newVal) { //一个给属性提供 setter 的方法(给对象属性设置值时调用的函数),如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined
					self.data[key] = newVal;
				}
			});
		}
	}
	function Observer(data) {
		this.data = data; /*data*/
		this.walk(data);
	}
	Observer.prototype = {
		walk: function(data) {
			let self = this;
			Object.keys(data).forEach(function(key) {
				self.defineReactive(data, key, data[key]); //访问或修改data中的每一个属性
			});
		},
		defineReactive: function(data, key, val) {
			let dep = new Dep();
			let childObj = observe(val);
			Object.defineProperty(data, key, {
				enumerable: true,
				configurable: true,
				get: function getter() { //缓存实体,new出来的 dep数组用来保存数据
					if (Dep.target) {
						dep.addSub(Dep.target);
					}
					return val;
				},
				set: function setter(newVal) { /*更新缓存里的值(重新获取)*/
					if (newVal === val) {
						return;
					}
					val = newVal;
					dep.notify();
				}
			});
		}
	};
	function observe(value, vm) {
		if (!value || typeof value !== 'object') {
			return;
		}
		return new Observer(value);
	};
	function Dep() {
		this.subs = [];
	}
	Dep.prototype = {
		addSub: function(sub) {
			this.subs.push(sub); /*缓存*/
		},
		notify: function() {
			this.subs.forEach(function(sub) {
				sub.update(); /*缓存更新*/
			});
		}
	};
	/*编译展现*/
	function Compile(el, vm) {
		this.vm = vm;
		this.el = document.querySelector(el); /*当前模板dom对象*/
		this.fragment = null;
		this.init();
	}
	Compile.prototype = {
		init: function() {
			if (this.el) {
				this.fragment = this.nodeToFragment(this.el); //创建虚拟dom
				this.compileElement(this.fragment);
				this.el.appendChild(this.fragment);
			} else {
				console.log('Dom元素不存在');
			}
		},
		nodeToFragment: function(el) {
			let fragment = document.createDocumentFragment();
			let child = el.firstChild;
			while (child) {
				// 将Dom元素移入fragment中
				fragment.appendChild(child);
				child = el.firstChild
			}
			return fragment;
		},
		compileElement: function(el) {
			let childNodes = el.childNodes;
			let self = this;
			[].slice.call(childNodes).forEach(function(node) {
				let reg = /\{\{(.*)\}\}/; //用来判定是否为 {{*}} ;
				let text = node.textContent; //当前node(选中dom)的文本内容(就是要显示的文本等)
				if (self.isElementNode(node)) { //dom
					self.compile(node);
				} else if (self.isTextNode(node) && reg.test(text)) { //文本
					self.compileText(node, reg.exec(text)[1]);
				}
				if (node.childNodes && node.childNodes.length) { //递归调用
					self.compileElement(node);
				}
				
			});
		},
		compile: function(node) {
			let nodeAttrs = node.attributes; //用户属性集合
			let self = this;
			Array.prototype.forEach.call(nodeAttrs, function(attr) {
				let attrName = attr.name;
				if (self.isDirective(attrName)) { /*过滤用户属性保留V-指令*/
					let exp = attr.value; //获取 用户v-指令的值
					let dir = attrName.substring(2);
					if (self.isEventDirective(dir)) { // 是否为事件指令
						self.compileEvent(node, self.vm, exp, dir); //执行对应事件
					}else if(self.isattrDirective(dir)){
						dir=dir.substring(6);
						self.compileAttr(node, self.vm, exp, dir); /*v-model:属性 数据绑定编译处理*/
					}else { // 为v-model 指令
						self.compileModel(node, self.vm, exp, dir); /*v-model 数据绑定编译处理*/
					}
					node.removeAttribute(attrName);
					
				}
			});
		},
		compileText: function(node, exp) {
			let self = this;
			let initText = this.vm[exp];
			this.updateText(node, initText);
			new Watcher(this.vm, exp, function(value) {
				self.updateText(node, value);
				self.compilewatch(node, self.vm, exp);
			});
		},
		compileEvent: function(node, vm, exp, dir) {/*事件*/
		    let self = this;
			let eventType = dir.split(':')[1];
			let ev=self.getargs(exp);
			let args='';
			if(ev){
				args=ev;
				let zk=exp.indexOf('(');
				exp=exp.substr(0,zk)
			}
			let cb = vm.methods && vm.methods[exp];
			if (eventType && cb) {
				node.addEventListener(eventType,cb.bind(vm,args), true);
				//node.addEventListener(eventType, cb.apply(vm,args), false);
				//node.addEventListener(eventType,cb.call(vm,'name','age'), false);
			}
		},
		getargs:function(exp){
			let reg = /\((.*)\)/; 
			let args;
			if(reg.test(exp)){
				args=reg.exec(exp)[1].split(",");
				let reg1 = /\"(.*)\"/,
				    reg2 = /\'(.*)\'/;
				for(let i in args){
					if(reg1.test(args[i])){args[i]=reg1.exec(args[i])[1]}
					if(reg2.test(args[i])){args[i]=reg2.exec(args[i])[1]}
				}
			}
		    return args;
		},
		compilewatch:function(node, vm, exp) {/*监听值改变*/
   		    let wt = vm.watchs && vm.watchs[exp];
			if (wt&&iswatch) {
	             wt.call(vm);
			}
		},
		compileModel: function(node, vm, exp, dir) {
			
			let self = this;
			let val = this.vm[exp];
			this.modelUpdater(node, val,"value"); /*数据赋给dom*/
			new Watcher(this.vm, exp, function(value) {
				self.modelUpdater(node, value,"value");
				self.compilewatch(node, self.vm, exp);
			});
			node.addEventListener('input', function(e) { //dom 值赋给数据
				let newValue = e.target.value;
				if (val === newValue) {
					return;
				}
				self.vm[exp] = newValue; //复制
				val = newValue;
			});
		},
		compileAttr: function(node, vm, exp, dir) {
			let self = this;
			let val = this.vm[exp];
			this.modelUpdater(node, val,dir); /*数据赋给dom*/
			new Watcher(this.vm, exp, function(value) {
				self.modelUpdater(node, value,dir);
				self.compilewatch(node, self.vm, exp);
			});
		},
		updateText: function(node, value) {
			node.textContent = typeof value == 'undefined' ? '' : value; //更新dom  {{*}}的值
		},
		modelUpdater: function(node, value, attr) {
			node[attr] = typeof value == 'undefined' ? '' : value; //更新 attr属性的值
		},
		isDirective: function(attr) {
			return attr.indexOf('v-') == 0;//指令
		},
		isEventDirective: function(dir) {
			return dir.indexOf('on:') === 0;//事件
		},
		isattrDirective: function(dir) {
			return dir.indexOf(':') === 4||dir.indexOf(':') === 5&&dir.indexOf('on:') === -1;//属性
		},
		isElementNode: function(node) {
			return node.nodeType == 1;
		},
		isTextNode: function(node) {
			return node.nodeType == 3;
		}
	}
	/*数据缓存*/
	Dep.target = null; /*用来缓存遍历当前this*/
	function Watcher(vm, exp, cb) {
		this.cb = cb;
		this.vm = vm;
		this.exp = exp;
		this.value = this.get(); // 获取缓存的当前属性的值
	}
	Watcher.prototype = {
		update: function() {
			this.run(); /*更新值*/
		},
		run: function() {
			let value = this.vm.data[this.exp]; /*获取实时的值*/
			let oldVal = this.value;
			if (value !== oldVal) {
				this.value = value;
				this.cb.call(this.vm, value, oldVal);
			}
		},
		get: function() {
			Dep.target = this; // 缓存自己
			let value = this.vm.data[this.exp] // 从缓存里获取当前属性的值
			Dep.target = null; // 释放自己
			return value;
		}
	};
	return new DataBinginit(dataoptions);
}
复制代码
  • 扩展Date.prototype.format支持Date.format格式化date调用代码
	let me = this;
	let date = new Date();
	return date.format("当前时间为:YYYY-MM-DD,星期W,为第Q季度,时间为:hh:mm:ss:c");
}
Date.prototype.format = function (format) {
	//new Date( year, month, date, hrs, min, sec)
	//new Date()       //参数可以为整数  也可以为字符串  但格式必须正确  example: new Date(2009,1,1)        //正确  new Date("2009/1/1")     //正确
	//example  new Date().format( "当前日期为:YYYY-MM-DD,星期W,为第Q季度,时间为:hh:mm:ss:c")
	let o = {
		"Y+": this.getFullYear() + '',
		"M+": this.getMonth() + 1, //month  MM
		"D+": this.getDate(), //day  DD
		"h+": this.getHours(), //hour  hh
		"m+": this.getMinutes(), //minute mm
		"s+": this.getSeconds(), //second ss
		"Q+": Math.floor((this.getMonth() + 3) / 3), //quarter 季度 q
		"c+": this.getMilliseconds(), //millisecond 毫秒 c
		"W": ['一', '二', '三', '四', '五', '六', '日'][this.getDay() - 1] //week 星期
	}
	for (let k in o) {
		if (new RegExp("(" + k + ")").test(format)) {
			format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr((("" + o[k]).length >= 2 ? 2 : ("" + o[k]).length)))
		}
	}
	return format
}
复制代码
  • 页面调用
<html lang="cn">
<head>
    <meta charset="UTF-8">
    <title>双向数据绑定原理</title>
</head>
<style>
    #app {
        text-align: center;
    }
</style>
<body>
    <div id="app">
        <h2>{{title}}</h2>
		<!--<input v-model="name">-->
		<input id="input" v-model="name" placeholder="请输入" v-bind:name="attrname"></input>
        <h1>{{name}}</h1>
		<h1>{{name1}}</h1>
		<h1>当前时间为<span>{{date}}</span></h1>
        <button v-on:click="clickMe('1','2')">click me 来清空input输入框值!</button>
		<button v-on:click="getval">获取input输入框值!</button>
		<button v-on:click="clickto">click me try!</button>
    </div>
</body>
<script src="https://juejin.cn/post/index.js"></script>
<script type="text/javascript">
     new DataBing({
        el: '#app',
        data(){
            return{
                title: 'hello world!',
                name: '333',
                name1: '哈哈哈',
                attrname: "attrname",
                date: getNowDateStr(),
            }
        },
        methods: {
            clickMe: function (c1,c2) {
			    var ev = window.event;
                this.name ='';
				this.attrname = 'attrname';
				console.log(ev,c1,c2)
            },
			clickto:function () {
                this.name = 'hello world';
				this.attrname = '测试'+getNowDateStr();
            },
			getval:function(){
			   alert(this.name);
			}
        },
		watchs: {
		   name:function(){
		     console.log(getNowDateStr()+'输入框的值为 :  '+this.name);
		   },
		   attrname:function(){
		      console.log('输入框的name为:'+ this.attrname);
		   }
		},
        mounted: function () {
            window.setInterval(() => {
                this.date=getNowDateStr();
            }, 1000);
        }
    });

</script>
</html>

复制代码
  • vue3 proxy语法详解,可以借助proxy自行实现一个简单的双向数据绑定
// Proxy 用于修改某些操作的默认行为, 等同于在语言层面做出修改, 所以属于一种“ 元编程”( meta programming), 即对编程语言进行编程。
// Proxy 可以理解成, 在目标对象之前架设一层“ 拦截”, 外界对该对象的访问, 都必须先通过这层拦截, 因此提供了一种机制, 可以对外界的访问进行过滤和改写。 Proxy 这个词的原意是代理, 用在这里表示由它来“ 代理” 某些操作, 可以译为“ 代理器”。

// Proxy 支持的拦截操作一览, 一共 13 种。
// get(target, propKey, receiver): 拦截对象属性的读取, 比如proxy.foo和proxy['foo']。
// set(target, propKey, value, receiver): 拦截对象属性的设置, 比如proxy.foo = v或proxy['foo'] = v, 返回一个布尔值。
// has(target, propKey): 拦截propKey in proxy的操作, 返回一个布尔值。
// deleteProperty(target, propKey): 拦截delete proxy[propKey] 的操作, 返回一个布尔值。
// ownKeys(target): 拦截Object.getOwnPropertyNames(proxy)、 Object.getOwnPropertySymbols(proxy)、 Object.keys(proxy)、 for...in循环, 返回一个数组。 该方法返回目标对象所有自身的属性的属性名, 而Object.keys() 的返回结果仅包括目标对象自身的可遍历属性。
// getOwnPropertyDescriptor(target, propKey): 拦截Object.getOwnPropertyDescriptor(proxy, propKey), 返回属性的描述对象。
// defineProperty(target, propKey, propDesc) : 拦截Object.defineProperty(proxy, propKey, propDesc)、 Object.defineProperties(proxy, propDescs) , 返回一个布尔值。
// preventExtensions(target) : 拦截Object.preventExtensions(proxy) , 返回一个布尔值。
// getPrototypeOf(target) : 拦截Object.getPrototypeOf(proxy) , 返回一个对象。
// isExtensible(target) : 拦截Object.isExtensible(proxy) , 返回一个布尔值。
// setPrototypeOf(target, proto) : 拦截Object.setPrototypeOf(proxy, proto) , 返回一个布尔值。 如果目标对象是函数, 那么还有两种额外操作可以拦截。
// apply(target, object, args) : 拦截 Proxy 实例作为函数调用的操作, 比如proxy(...args) 、 proxy.call(object, ...args) 、 proxy.apply(...) 。
// construct(target, args) : 拦截 Proxy 实例作为构造函数调用的操作, 比如new proxy(...args) 。

// 技巧: 一个技巧是将 Proxy 对象, 设置到object.proxy属性, 从而可以在object对象上调用。
var object = { proxy: new Proxy(target, handler) };

var handler = {
  get: function(target, name) {
    console.log("get")
    if (name === 'prototype') {
      return Object.prototype;
    }
    return 'Hello, ' + name;
  },
  set: function(target, thisBinding, args) {
    console.log("set")
    return 20;
  },
  apply: function(target, thisBinding, args) {
    console.log("apply")
    return args[0];
  },
  construct: function(target, args) {
    console.log("construct")
    return { value: args[1] };
  }
};

var fproxy = new Proxy({}, handler);
var fproxy2 = new Proxy(function(x, y) {
  return x + y;
}, handler);
let obj = Object.create(proxy);
fproxy2(1, 2)


// 虽然 Proxy 可以代理针对目标对象的访问, 但它不是目标对象的透明代理, 即不做任何拦截的情况下, 也无法保证与目标对象的行为一致。 主要原因就是在 Proxy 代理的情况下, 目标对象内部的this关键字会指向 Proxy 代理。

// 有些原生对象的内部属性, 只有通过正确的this才能拿到, 所以 Proxy 也无法代理这些原生对象的属性。这时, this绑定原始对象, 就可以解决这个问题。

const target = new Date('2015-01-01');
const handler = {
  get(target, prop) {
    if (prop === 'getDate') {
      return target.getDate.bind(target);
    }
    return Reflect.get(target, prop);
  }
};
const proxy = new Proxy(target, handler);
proxy.getDate() // 1

// ES5 提供了 Object.defineProperty 方法, 该方法可以在一个对象上定义一个新属性, 或者修改一个对象的现有属性, 并返回这个对象。

// Object.defineProperty(obj, prop, descriptor)
// 参数:
// obj: 要在其上定义属性的对象。
// prop: 要定义或修改的属性的名称。
// descriptor: 将被定义或修改的属性的描述符。
// 下面一个简单的输入框变化, 数据展示

var obj = {
  value: ''
}
var value = '';
Object.defineProperty(obj, "value", {
  get: function() {
    return value
  },
  set: function(newVal) {
    value = newVal
  }
})
document.querySelector('#input').oninput = function() {
  var value = this.value;
  obj.value = value;
  document.querySelector('#text').innerHTML = obj.value;
}
// 当然一般不会只是改变一个属性, 如下所示, 遍历劫持对象所有的属性

//要劫持的对象
const data = {
  name: ''
}
//遍历对象,对其属性值进行劫持
Object.keys(data).forEach(function(key) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      console.log('get');
    },
    set: function(newVal) {
      // 当属性值发生变化时我们可以进行额外操作
      console.log(`我修改了成了${newVal}`);
    },
  })
})
data.name = 'gg'
// 这个只是简单的劫持
// 缺点: Object.defineProperty的第一个缺陷, 无法监听数组变化
// 区别:
// Proxy可以直接监听对象而非属性
// Proxy直接可以劫持整个对象, 并返回一个新对象, 不管是操作便利程度还是底层功能上都远强于Object.defineProperty。
// Proxy可以直接监听数组的变化
// Proxy有多达13种拦截方法, 不限于apply、 ownKeys、 deleteProperty、 has等等是Object.defineProperty不具备的。
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享