原生方式实现vue数据的双向绑定

本文详细介绍了如何通过原生JavaScript实现Vue的数据双向绑定。从创建组件开始,利用CustomElementRegistry定义自定义元素,解析元素内容,绑定数据视图,并处理事件,最终实现完整的双向数据绑定功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.创建组件
// html
	// 1.创建组件内容 
	<template id='userCardTemplate'>
		<style type="text/css">
			.container{
				background: #eee;
				border-radius: 10px;
				width: 500px;
				padding: 20px
			}
		</style>
		<div class="container">
			<p class="name" data-open='true'>{{name}}</p>
			<p class="email">{{email}}</p>
			<input type="text" v-model='message'>
			<span>{{message}}</span>
			<button class="button">Follow</button>
		</div>
	</template>

	<user-card data-click="123"></user-card>

	

2.组件类

customElements是Window对象上的一个只读属性,接口返回一个CustomElementRegistry对象的引用,可用于注册新的custom elements,或者获取之前定义过的自定义元素的信息。
CustomElementRegistry.define()方法用来注册一个custom element,该方法接收以下参数:
1.表示所创建的元素名称符合DOMString标准的字符串。注意,custom element的名称不能是个单个单词,且其中必须要有短横线。
2.用于定义元素行为的类。
3.可选参数,一个包含extends属性的配置对象,是可选参数,它指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。

// js
class UserCard extends HTMLElement{
		constructor(){
			super();
			var templateEle = document.getElementById('userCardTemplate');
			var content = templateEle.content.cloneNode(true);
			this.appendChild(content);
			this._data={
				name:'用户名',
				email:'yourmail@some-email.com',
				message:'双向'
			};
		}
	}
	window.customElements.define('user-card',UserCard);
效果图

在这里插入图片描述

3.解析

接下来要做的事情就是解析元素里面的子元素,看看里面是不是包含了{{}}这样的符号,并且要把中间的内容拿出来,和data里面的数据进行比对,如果对应上了,那么就把数据填充到这个里面

// 解析
	compileNode(el){
		let child = el.childNodes;// 获取到所有子元素
		[...child].forEach((node)=>{ // 利用展开运算符直接转换成数组然后forEach
			if(node.nodeType == 3){   // nodeType 获取节点类型  nodeType == 3; 代表元素或属性中的文本内容
				let text = node.textContent;
				// 匹配前面有两个{{,后面也有两个}}的这么一串文本
				let reg = /\{\{\s*([^\s\{\}]+)\s*}\}/g;
				if(reg.test(text)){ // 如果能找到这样的字符串
					// 将里面的数据拿出来,比如‘name’
					let $1 = RegExp.$1;  // 正则表达式匹配的第一个子匹配(以括号为标志)字符串
					// 看看数据里面有没有name这个东西,如果有,那就把数据里面name对应的值天道当前这个位置
					this._data[$1]&&(node.textContent = text.replace(reg,this._data[$1]));

					// 增加了事件监听,监听每一个匹配到的数据,并且再一次更新视图;
					// 注意这里的e.detail是上面observe里面的自定义事件传过来的
					this.addEventListener($1,(e)=>{
						node.textContent = text.replace(reg,e.detail)
					})
				}
			}else if(node.nodeType ==1){ // 实现数据双向绑定 nodeType == 1; 代表元素
				let attrs = node.attributes;  // 获取元素属性的集合
				if(attrs.hasOwnProperty('v-model')){ // 判断是不是有这个属性
					let keyname = attrs['v-model'].nodeValue;  // 属性的值
					node.value = this._data[keyname];
					node.addEventListener('input',(e)=>{ // 如果有,监听事件,修改数据
						this._data[keyname] = node.value ; //修改数据
					})
				}

				if(node.childNodes.length>0){
					this.compileNode(node) ; // 递归实现深度解析
				}
			}
		})
	}

效果图

在这里插入图片描述

4.实现数据视图绑定

// 实现数据视图绑定
	observe(){
		let _this = this;
		/*
			Proxy的意思是代理,其作用是可以拦截对象上的一个操作;用法如下:
			通过new的方式创建对象,第一个参数是被拦截的对象,第二个参数是对象操作的描述。实例化后返回一个新的对象,当我们对这个新的对象进行操作时就会调用我们描述中对应的方法。
			Proxy区别于Object.defineProperty:
			Object.defineProperty只能监听到属性的读写,而Proxy除读写外还可以监听属性的删除,方法的调用等。
			通常情况下我们想要监听数组的变化,基本要依靠重写数组方法的方式实现,这也是Vue的实现方式,而Proxy可以直接监听数组的变化。
			Proxy是已非入侵的方式监管了对象的读写,而defineProperty需要按特定的方式定义对象的属性
		*/
		this._data = new Proxy(this._data,{ // 监听数据
			set(obj,prop,value){ // 数据改变的时候会触发set方法
				// 事件通知机制,发生改变的时候,通过自定义事件通知视图发生改变
				let event = new CustomEvent(prop,{
					detail: value // 注意这里我传了个detail过去,这样的话更新视图的时候就可以直接拿到新的数据
				});
				_this.dispatchEvent(event); // 自定义事件的触发

				// Refect是一个内建的对象,用来提供法法拦截JavaScript的操作。Reflect不是一个函数对象,所以它是不可构造的,也就是说它不是一个构造器,你不能通过 new 操作符去新建或者将其作为一个函数去调用Reflect对象,Reflect的所有属性和方法都是静态的
				return Reflect.set(...arguments); // 这里是为了确保修改成功,不写其实也么有关系
			}
		})
	}

到这一步,我们可以实现修改数据的时候,视图也发生改变

window.customElements.define('user-card',UserCard);
let card = document.querySelector('user-card');
document.onclick = function(){
	console.log('点击了');
	card._data.name = "新数据"
}

效果图

在这里插入图片描述

5.处理事件

bindEvent(){
	this.event = new popEvent({
		obj: this,
		popup: true
	});
}

class popEvent{
	constructor(option){
		/*
              * 接收四个参数:
              * 1,对象的this
              * 2,要监听的元素
              * 3,要监听的事件,默认监听点击事件
              * 4,是否冒泡
              * */
              this.eventObj = option.obj;
              this.target = option.target || this.eventObj;
              this.eventType = option.eventType || 'click';
              this.popup = option.popup || false;
              this.bindEvent();
	}
	bindEvent(){
		let _this = this;
		_this.target.addEventListener(_this.eventType,function(ev){
			console.log(_this.eventType,'eventType')
			let target = ev.target;
			let dataset,parent,num,b;
			popup(target);
			function popup(obj){
				if(obj === document){return false}
				/*
					HTMLElement.dataset属性允许无论实在读取模式和写入模式下访问在HTML或DOM中的元素上设置的所有自定义数据属性(data-*)集。
					它是一个DOMString的映射,每个自定义数据属性的一个条目
				*/
				dataset = obj.dataset;
				/*
					Object.keys(obj)
					参数:要返回其枚举自身属性的对象
					返回值:一个表示给定对象的所有可枚举属性的字符串数组
				*/
				num = Object.keys(dataset).length;
				parent = obj.parentNode;
				if(num<1){
					_this.popup && popup(parent);
					num = 0;
				}else {
					for(b in dataset){
						if(_this.eventObj.__proto__[b]){
							_this.eventObj.__proto__[b].call(_this.eventOjb,{obj:obj,ev:ev,target:dataset[b],data:_this.eventOjb})
						}
					}
					_this.popup && popup(parent);
				}
			}
		})
	}
}

完整代码

<!DOCTYPE html>
<html>
<head>
	<title></title>
</head>
<body>
	<!-- 1.创建组件内容 -->
	<template id='userCardTemplate'>
		<style type="text/css">
			.container{
				background: #eee;
				border-radius: 10px;
				width: 500px;
				padding: 20px
			}
		</style>
		<div class="container">
			<p class="name" data-open='true'>{{name}}</p>
			<p class="email">{{email}}</p>
			<input type="text" v-model='message'>
			<span>{{message}}</span>
			<button class="button">Follow</button>
		</div>
	</template>

	<user-card data-click="123"></user-card>

	<script type="module">

		class popEvent{
			constructor(option){
				/*
                * 接收四个参数:
                * 1,对象的this
                * 2,要监听的元素
                * 3,要监听的事件,默认监听点击事件
                * 4,是否冒泡
                * */
                this.eventObj = option.obj;
                this.target = option.target || this.eventObj;
                this.eventType = option.eventType || 'click';
                this.popup = option.popup || false;
                this.bindEvent();
			}
			bindEvent(){
				let _this = this;
				_this.target.addEventListener(_this.eventType,function(ev){
					console.log(_this.eventType,'eventType')
					let target = ev.target;
					let dataset,parent,num,b;
					popup(target);
					function popup(obj){
						if(obj === document){return false}
						/*
							HTMLElement.dataset属性允许无论实在读取模式和写入模式下访问在HTML或DOM中的元素上设置的所有自定义数据属性(data-*)集。
							它是一个DOMString的映射,每个自定义数据属性的一个条目
						*/
						dataset = obj.dataset;
						/*
							Object.keys(obj)
							参数:要返回其枚举自身属性的对象
							返回值:一个表示给定对象的所有可枚举属性的字符串数组
						*/
						num = Object.keys(dataset).length;
						parent = obj.parentNode;
						if(num<1){
							_this.popup && popup(parent);
							num = 0;
						}else {
							for(b in dataset){
								if(_this.eventObj.__proto__[b]){
									_this.eventObj.__proto__[b].call(_this.eventOjb,{obj:obj,ev:ev,target:dataset[b],data:_this.eventOjb})
								}
							}
							_this.popup && popup(parent);
						}
					}
				})
			}
		}

		class UserCard extends HTMLElement{
			constructor(){
				super();
				var templateEle = document.getElementById('userCardTemplate');
				var content = templateEle.content.cloneNode(true);
				this.appendChild(content);
				this._data={
					name:'用户名',
					email:'yourmail@some-email.com',
					message:'双向'
				};
				this.compileNode(this); // 解析元素
				this.observe(this._data); //监听数据
				this.bindEvent(); //处理事件
				this.addevent = this.__proto__;
			}
			bindEvent(){
				this.event = new popEvent({
					obj: this,
					popup: true
				});
			}
			// 实现数据视图绑定
			observe(){
				let _this = this;
				/*
					Proxy的意思是代理,其作用是可以拦截对象上的一个操作;用法如下:
					通过new的方式创建对象,第一个参数是被拦截的对象,第二个参数是对象操作的描述。实例化后返回一个新的对象,当我们对这个新的对象进行操作时就会调用我们描述中对应的方法。
					Proxy区别于Object.defineProperty:
					Object.defineProperty只能监听到属性的读写,而Proxy除读写外还可以监听属性的删除,方法的调用等。
					通常情况下我们想要监听数组的变化,基本要依靠重写数组方法的方式实现,这也是Vue的实现方式,而Proxy可以直接监听数组的变化。
					Proxy是已非入侵的方式监管了对象的读写,而defineProperty需要按特定的方式定义对象的属性
				*/
				this._data = new Proxy(this._data,{ // 监听数据
					set(obj,prop,value){ // 数据改变的时候会触发set方法
						// 事件通知机制,发生改变的时候,通过自定义事件通知视图发生改变
						let event = new CustomEvent(prop,{
							detail: value // 注意这里我传了个detail过去,这样的话更新视图的时候就可以直接拿到新的数据
						});
						_this.dispatchEvent(event); // 自定义事件的触发

						// Refect是一个内建的对象,用来提供法法拦截JavaScript的操作。Reflect不是一个函数对象,所以它是不可构造的,也就是说它不是一个构造器,你不能通过 new 操作符去新建或者将其作为一个函数去调用Reflect对象,Reflect的所有属性和方法都是静态的
						return Reflect.set(...arguments); // 这里是为了确保修改成功,不写其实也么有关系
					}
				})
			}
			// 解析
			compileNode(el){
				let child = el.childNodes;// 获取到所有子元素
				[...child].forEach((node)=>{ // 利用展开运算符直接转换成数组然后forEach
					if(node.nodeType == 3){   // nodeType 获取节点类型  nodeType == 3; 代表元素或属性中的文本内容
						let text = node.textContent;
						// 匹配前面有两个{{,后面也有两个}}的这么一串文本
						let reg = /\{\{\s*([^\s\{\}]+)\s*}\}/g;
						if(reg.test(text)){ // 如果能找到这样的字符串
							// 将里面的数据拿出来,比如‘name’
							let $1 = RegExp.$1;  // 正则表达式匹配的第一个子匹配(以括号为标志)字符串
							// 看看数据里面有没有name这个东西,如果有,那就把数据里面name对应的值天道当前这个位置
							this._data[$1]&&(node.textContent = text.replace(reg,this._data[$1]));

							// 增加了事件监听,监听每一个匹配到的数据,并且再一次更新视图;
							// 注意这里的e.detail是上面observe里面的自定义事件传过来的
							this.addEventListener($1,(e)=>{
								node.textContent = text.replace(reg,e.detail)
							})
						}
					}else if(node.nodeType ==1){ // nodeType == 1; 代表元素
						let attrs = node.attributes;  // 获取元素属性的集合
						if(attrs.hasOwnProperty('v-model')){ // 判断是不是有这个属性
							let keyname = attrs['v-model'].nodeValue;  // 属性的值
							node.value = this._data[keyname];
							node.addEventListener('input',(e)=>{ // 如果有,监听事件,修改数据
								this._data[keyname] = node.value ; //修改数据
							})
						}

						if(node.childNodes.length>0){
							this.compileNode(node) ; // 递归实现深度解析
						}
					}


				})
			}
			
			open(){
				/*
				template里面的代码:<p class="name" data-open="true">{{name}}</p>,这是实现事件指令当点击含有自定义属性:data-open的元素的时候,就可以触发组件里的open方法,并且在open方法里还能够得到任何你需要的参数。
				*/
				console.log("触发了open方法")
			}
		}
		
		/*
			customElements是Window对象上的一个只读属性,接口返回一个CustomElementRegistry对象的引用,可用于注册新的custom elements,或者获取之前定义过的自定义元素的信息。
			CustomElementRegistry.define()方法用来注册一个custom element,该方法接收以下参数:
			1.表示所创建的元素名称符合DOMString标准的字符串。注意,custom element的名称不能是个单个单词,且其中必须要有短横线。
			2.用于定义元素行为的类。
			3.可选参数,一个包含extends属性的配置对象,是可选参数,它指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。
		*/
		window.customElements.define('user-card',UserCard);

		let card = document.querySelector('user-card');
		card.addevent['click'] = function(){
			console.log('触发了点击事件')
		}
		
	</script>
</body>
</html>

最后效果图

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值