双向绑定的原理

双向绑定的核心原理

核心是采用数据劫持结合发布者订阅者模式,通过Object.defineProperty()对每个属性的get和set进行拦截。在数据发生变化的时候发布消息给订阅者,触发相应的监听回调。
仅仅使用Object,defineProperty()就能完成一个简单的双向绑定,但是效率比较低。观察者模式让双向绑定更有效率,它是一对多的模式,一指的是修改的一处数据,多是凡是用了这个数据的地方都更新。数据劫持就是利用Object.defineProperty()读操作数据的set,get。

极简的双向绑定

Object.defineProperty()有三个参数,第一个是属性所在的对象。第二个是要操作的属性,第三个是被操作的属性的特性,是一个对象{},一般是两个get:读取属性时触发,set写入属性时触发。

<input type="text" id="inputtext">
<p id="ptext"></p>
let obj={}
Object.defineProperty(obj,'val',{
	get:function(){},
	set:function(_val){
		document.getElementById("inputtext").value=_val
		document.getElementById('ptext').innerHTML=_val
	}
})
document.addEventListener("keyup",function(e){
	obj.val=e.target.value
})

简单的发布订阅模式

1.Dep类
负责进行依赖收集
首先要有一个数组,存放订阅信息
其次提供一个向数组中追加订阅信息的方法
然后提供一个循环,循环触发数组中的每个订阅信息
2.watcher类
负责订阅一些事件

//收集依赖/收集订阅者
class Dep{
  constructor(){
    //存放订阅者的数组
    this.subs=[]
  }
  //将订阅者存放到数组中
  add(watcher){
    this.subs.push(watcher)
  }
  //发布通知的方法
  notify(){
     this.subs.forEach(watcher=>{
       return watcher.update()
     })
  }
}
//订阅者的类
class Watcher{
  //接收一个回调函数,将回调函数挂载到自己身上
  constructor(callback){
    this.callback=callback
  }
  //触发回调的方法
  update(){
    this.callback()
  }
}
let w1=new Watcher(()=>{console.log('第一个订阅者')})
let w2=new Watcher(()=>{console.log('第二个订阅者')})
let dep=new Dep()
dep.add(w1) 
dep.add(w2)
//只要我们为vue中data的数据重新赋值,vue会监听到,并把数据的变化通知到订阅者,订阅者(DOM元素)会根据最新的数据,更新内容
dep.notify()

Object.defineProperty()

let obj={
  name:'lisa',
  age:23
}
Object.defineProperty(obj,'name',{
  enumerable:true, //当前属性允许被循环
  configurable:true,//当前属性允许被配置
  get(){  //getter
    console.log("name属性被访问")
  },
  set(newVal){ //setter
    console.log("name属性被修改为"+newVal)
  }
})
obj.name  //访问
obj.name="zhao"  //修改

实现数据劫持

<div id="app">
		姓名是:{{name}}
		年龄:{{age}}
	</div>
const vm = new Vue({
			el: '#app',
			data: {
				name: 'lisa',
				age: 21,
				info: {
					a: "a1",
					b: "b1"
				}
			},
		})
		console.log(vm)
class Vue {
			constructor(options) {
				this.$data = options.data
				//调用数据劫持的方法
				Observer(this.$data)
				//属性代理,不使用代理时,访问数据this.$data.xx
				//使用代理后访问this.xxx
				Object.keys(this.$data).forEach(key=>{
					//为this添加了key属性
					Object.defineProperty(this,key,{
						enumerable:true,
						configurable:true,
						get(){
							return this.$data[key]
						},
						set(newVal){
							this.$data[key]=newVal
						}
					})
				})
			}
		}
		//定义一个数据劫持的方法
		function Observer(obj) {
			//递归的终止条件
			if (!obj || typeof obj !== "object") return
			//获取obj的属性并添加set,get
			Object.keys(obj).forEach((key) => {
				let value = obj[key]
				//把value这个子节点进行递归
				Observer(value)
				Object.defineProperty(obj, key, {
					enumerable: true,
					configurable: true,
					//属性被访问时,返回value
					get() {
						console.log(`有人获取了${key}的值`)
						return value
					},
					set(newVal) {
						value = newVal
						//给属性重新赋值后,新的属性值没有了get和set,需要重新递归
						Observer(value)
					}
				})
			})
		}

模板编译

创建文档碎片,提高DOM操作的性能

在浏览器中,一旦把节点插入到document.body(或其他节点)中,页面就会更新并反映出这种变化。但是当要向document添加大量数据时,这个过程就可能会十分缓慢。
为了解决这个问题,我们可以引入createDocumentFragment()方法,它的作用是创建一个文档碎片,把要插入的新节点先附加在它上面,然后再一次性添加到document中

function Compile(el,vm){
			//获取el对应的DOM元素
			vm.$el=document.querySelector(el)
			//创建文档碎片
			const fragment=document.createDocumentFragment()
			//当存在子节点时,将节点加入到文档碎片中
			while(childNode=vm.$el.firstChild){
				fragment.appendChild(childNode)
			}
			//进行模板编译
			replace(fragment);
			//将文档碎片中的节点加入到DOM节点中
			vm.$el.appendChild(fragment)
			//创建一个对模板进行编译的方法
			function replace(node){
				//定义匹配插值表达式的正则
				const regMustache=/\{\{\s*(\S+)\s*\}\}/
				//判断如果为纯文本节点,则进行文本的替换
				if(node.nodeType ===3){
					const text=node.textContent
					//数组第一个值为替换前的值,第二个值为替换后的值
					const exec=regMustache.exec(text)
					console.log(exec)
					//如果内容存在(空格也为一个文本节点)
					if(exec){
						//对值进行分割,再拿到最下面的值
						let value=exec[1].split('.').reduce((newObj,k)=>{
							return newObj[k]
						},vm)
						node.textContent=text.replace(regMustache,value)
					}
					return
				}
				//不是纯文本节点,可能是DOM元素需要进行递归处理
				node.childNodes.forEach(child=>{
					replace(child)
				})
			}
		}

创建Dep累进行依赖收集

class Dep{
			constructor(){
				this.subs=[]
			}
			addSub(watcher){
				this.subs.push(watcher)
			}
			//负责通知每个watcher的方法
			notify(){
				this.subs.forEach(watcher=>
					watcher.update()
				)
			}
		}

完整的双向绑定

<div id="app">
		<h3>姓名是:{{name}}</h3>
		<h3>年龄:{{age}}</h3>
		<h3>a的值是:{{info.a}}</h3>
		姓名是:<input type="text" v-model="name">
	</div>
const vm = new Vue({
			el: '#app',
			data: {
				name: 'lisa',
				age: 21,
				info: {
					a: "a1",
					b: "b1"
				}
			},
		})
class Vue {
			constructor(options) {
				this.$data = options.data
				//调用数据劫持的方法
				Observer(this.$data)
				//属性代理,不使用代理时,访问数据this.$data.xx
				//使用代理后访问this.xxx
				Object.keys(this.$data).forEach(key=>{
					//为this添加了key属性
					Object.defineProperty(this,key,{
						enumerable:true,
						configurable:true,
						get(){
							return this.$data[key]
						},
						set(newVal){
							this.$data[key]=newVal
						}
					})
				})
				//调用模板编译的函数
				Compile(options.el,this)
			}
		}
		//定义一个数据劫持的方法
		function Observer(obj) {
			//递归的终止条件
			if (!obj || typeof obj !== "object") return
			const dep=new Dep()
			//获取obj的属性并添加set,get
			Object.keys(obj).forEach((key) => {
				let value = obj[key]
				//把value这个子节点进行递归
				Observer(value)
				Object.defineProperty(obj, key, {
					enumerable: true,
					configurable: true,
					//属性被访问时,返回value
					get() {
						// console.log(`有人获取了${key}的值`)
						//将watcher的实例放到dep.subs数组中
						Dep.target && dep.addSub(Dep.target)
						return value
					},
					set(newVal) {
						value = newVal
						//给属性重新赋值后,新的属性值没有了get和set,需要重新递归
						Observer(value)
						//通知每个订阅者更新自己的文本
						dep.notify()
					}
				})
			})
		}
		//对HTML结构进行模板编译的方法
		function Compile(el,vm){
			//获取el对应的DOM元素
			vm.$el=document.querySelector(el)
			//创建文档碎片
			const fragment=document.createDocumentFragment()
			//当存在子节点时,将节点加入到文档碎片中
			while(childNode=vm.$el.firstChild){
				fragment.appendChild(childNode)
			}
			//进行模板编译
			replace(fragment);
			//将文档碎片中的节点加入到DOM节点中
			vm.$el.appendChild(fragment)
			//创建一个对模板进行编译的方法
			function replace(node){
				//定义匹配插值表达式的正则
				const regMustache=/\{\{\s*(\S+)\s*\}\}/
				//判断如果为纯文本节点,则进行文本的替换
				if(node.nodeType ===3){
					const text=node.textContent
					//数组第一个值为替换前的值,第二个值为替换后的值
					const exec=regMustache.exec(text)
					//如果内容存在(空格也为一个文本节点)
					if(exec){
						//对值进行分割,再拿到最下面的值
						let value=exec[1].split('.').reduce((newObj,k)=>{
							return newObj[k]
						},vm)
						node.textContent=text.replace(regMustache,value)
						//在这个时候,创建watcher类的实例
						new Watcher(vm,exec[1],(newVal)=>{
							node.textContent=text.replace(regMustache,newVal)
						})
					}
					//终止递归的条件
					return
				}
				//判断是否为一个元素,并且是否为input输入框
				if(node.nodeType ===1 && node.tagName.toUpperCase() =="INPUT"){
					//得到当前元素的所有属性节点
					const attrs=Array.from(node.attributes)
					const findResult=attrs.find(x=>x.name ==="v-model"
					)
					if(findResult){
						//获取到当前v-model属性的值,v-modal="name",v-modal="info.a"
						const expStr=findResult.value
						const value=expStr.split(".").reduce((newObj,k)=>newObj[k],vm)
						node.value=value
						//创建watcher的实例
						new Watcher(vm,expStr,(newValue)=>{
							node.value=newValue
						})
						//监听文本框的input输入事件,拿到文本框最新的值,更新到vm上
						node.addEventListener('input',(e)=>{
							const keyArr=expStr.split('.')
							const obj=keyArr.slice(0,keyArr.length-1).reduce((newObj,k)=>newObj[k],vm)
							console.log(obj)
							obj[keyArr[keyArr.length-1]]=e.target.value
						})
					}
				}
				//不是纯文本节点,可能是DOM元素需要进行递归处理
				node.childNodes.forEach(child=>{
					replace(child)
				})
			}
		}
		//收集依赖/收集订阅者
		class Dep{
			constructor(){
				this.subs=[]
			}
			addSub(watcher){
				this.subs.push(watcher)
			}
			//负责通知每个watcher的方法
			notify(){
				this.subs.forEach(watcher=>
					watcher.update()
				)
			}
		}
		//订阅者的类
		class Watcher{
			//cb回调函数中,记录着当前watcher如何更新自己的文本内容,
			//但是,知道如何更新自己还不行,还必须拿到最新的数据
			//因此,还需要在new Watcher期间,把vm也传进来(因为vm中保存着最新的数据)
			//除此之外,还需要知道在vm的数据中,哪个才是自己需要的数据
			//因此,必须在new Watcher期间,指定watcher对应数据的名字
			constructor(vm,key,cb){
				this.vm=vm
				this.key=key
				this.cb=cb
				//下面3行负责把创建的watcher实例存放到subs数组中
				Dep.target=this //this指向watcher实例
				//取值目的是去执行get()
				key.split(".").reduce((newObj,key)=>newObj[key],vm)
				Dep.target=null
			}
			//watcher的实例,需要有update函数,从而让发布者能够通知我们进行更新
			update(){
				const value=this.key.split(".").reduce((newObj,key)=>newObj[key],this.vm)
				this.cb(value)
			}
		}
  • 5
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue的双向绑定原理是通过数据劫持和发布订阅模式相结合的方式来实现的。在Vue中,当用户操作View时,ViewModel会感知到变化并通知Model进行相应的改变;反之,当Model发生改变时,ViewModel也能感知到变化并使View作出相应的更新。双向绑定的核心是使用了Object.defineProperty()方法来实现。 在Vue的初始化过程中,会对data数据进行劫持监听,这个过程由监听器Observe来完成。监听器会监听所有属性,当属性发生变化时,会通知订阅者Watcher来判断是否需要更新。由于订阅者Watcher可能有多个,所以需要一个消息订阅器Dep来统一管理这些订阅者。同时,还需要一个指令解析器Compile,用来扫描和解析每个节点的相关指令,将其初始化为一个订阅者Watcher,并替换模板数据或绑定相应的函数。 当订阅者Watcher接收到属性的变化通知时,会执行对应的更新函数,从而更新视图。整个过程中,监听器Observer负责劫持并监听所有属性,订阅者Watcher负责接收属性的变化通知并执行相应的函数,消息订阅器Dep负责收集订阅者并通知Watcher触发更新,指令解析器Compile负责扫描和解析节点的指令并初始化相应的订阅者。 综上所述,Vue的双向绑定原理是通过数据劫持+发布订阅模式相结合的方式来实现的,通过监听器、订阅者、消息订阅器和指令解析器等组件的协作,实现了数据和视图之间的双向绑定。 #### 引用[.reference_title] - *1* *2* [vue双向绑定原理](https://blog.csdn.net/qq_41645323/article/details/123324680)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Vue双向绑定原理](https://blog.csdn.net/weixin_52092151/article/details/119810514)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值