vue如何实现简易的双向数据绑定

夏日的草原,早晨空气格外清新,我缠着父亲在草原上漫步。幽幽的草香迎面拂来,红艳艳。朝阳正从地平线上冉冉升起,为辽阔的草原镀上一层金色。草叶上的露珠,像镶在翡翠上的珍珠,闪着五颜六色的光华。我看到草丛中夹着许多粉红色、白色、黄色或是蓝色的不知名的花,把草原装扮得十分美丽。

主要是通过数据劫持和发布订阅一起实现的

  • 双向数据绑定 数据更新时,可以更新视图 视图的数据更新是,可以反向更新模型

组成说明

  • Observe监听器 劫持数据, 感知数据变化, 发出通知给订阅者, 在get中将订阅者添加到订阅器中
  • Dep消息订阅器 存储订阅者, 通知订阅者调用更新函数
  • 订阅者Wather取出模型值,更新视图
  • 解析器Compile 解析指令, 更新模板数据, 初始化视图, 实例化一个订阅者,将更新函数绑定到订阅者上, 可以在接收通知二次更新视图, 对于v-model还需要监听input事件,实现视图到模型的数据流动

基本结构

HTML模板

<divid="app">
<form>
<inputtype="text"v-model="username">
</form>
<pv-bind="username"></p>
</div>
  • 一个根节点#app
  • 表单元素,里面包含input, 使用v-model指令绑定数据username
  • p元素上使用v-bind绑定数username

MyVue类

简单的模拟Vue类

将实例化时的选项options, 数据options.data进行保存 此外,通过options.el获取dom元素,存储到$el上

classMyVue{
constructor(options){
this.$options=options
this.$el=document.querySelector(this.$options.el)
this.$data=options.data
}
}

实例化MyVue

实例化一个MyVue,传递选项进去,选项中指定绑定的元素el和数据对象data

constmyVm=newMyVue({
el:'#app',
data:{
username:'LastStarDust'
}
})

Observe监听器实现

劫持数据是为了修改数据的时候可以感知, 发出通知, 执行更新视图操作

classMyVue{
constructor(options){
// ...
//监视数据的属性
this.observable(this.$data)
}
//递归遍历数据对象的所有属性,进行数据属性的劫持{username:'LastStarDust'}
observable(obj){
//obj为空或者不是对象,不做任何操作
constisEmpty=!obj||typeofobj!=='object'
if(isEmpty){
return
}

//['username']
constkeys=Object.keys(obj)
keys.forEach(key=>{
//如果属性值是对象,递归调用
letval=obj[key]
if(typeofval==='object'){
this.observable(val)
}
//this.defineReactive(this.$data,'username','LastStarDust')
this.defineReactive(obj,key,val)
})

returnobj
}

//数据劫持,修改属性的get和set方法
defineReactive(obj,key,val){
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
console.log(`取出${key}属性值:值为${val}`)
returnval
},
set(newVal){
//没有发生变化,不做更新
if(newVal===val){
return
}
console.log(`更新属性${key}的值为:${newVal}`)
val=newVal
}
})
}
}

Dep消息订阅器

存储订阅者, 收到通知时,取出订阅者,调用订阅者的update方法

//定义消息订阅器
classDep{
//静态属性Dep.target,这是一个全局唯一的Watcher,因为在同一时间只能有一个全局的Watcher
statictarget=null
constructor(){
//存储订阅者
this.subs=[]
}
//添加订阅者
add(sub){
this.subs.push(sub)
}
//通知
notify(){
this.subs.forEach(sub=>{
//调用订阅者的update方法
sub.update()
})
}
}

将消息订阅器添加到数据劫持过程中

为每一个属性添加订阅者

defineReactive(obj,key,val){
constdep=newDep()
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
//会在初始化时,触发属性get()方法,来到这里Dep.target有值,将其作为订阅者存储起来,在触发属性的set()方法时,调用notify方法
if(Dep.target){
dep.add(Dep.target)
}
console.log(`取出${key}属性值:值为${val}`)
returnval
},
set(newVal){
//没有发生变化,不做更新
if(newVal===val){
return
}
console.log(`更新属性${key}的值为:${newVal}`)

val=newVal
dep.notify()
}
})
}

订阅者Wather

从模型中取出数据并更新视图

//定义订阅者类
classWather{
constructor(vm,exp,cb){
this.vm=vm//vm实例
this.exp=exp//指令对应的字符串值,如v-model="username",exp相当于"username"
this.cb=cb//回到函数更新视图时调用
this.value=this.get()//将自己添加到消息订阅器Dep中
}

get(){
//将当前订阅者作为全局唯一的Wather,添加到Dep.target上
Dep.target=this
//获取数据,触发属性的getter方法
constvalue=this.vm.$data[this.exp]
//在执行添加到消息订阅Dep后,重置Dep.target
Dep.target=null
returnvalue
}

//执行更新
update(){
this.run()
}

run(){
//从Model模型中取出属性值
constnewVal=this.vm.$data[this.exp]
constoldVal=this.value
if(newVal===oldVal){
returnfalse
}
//执行回调函数,将vm实例,新值,旧值传递过去
this.cb.call(this.vm,newVal,oldVal)
}
}

解析器Compile

  • 解析模板指令,并替换模板数据,初始化视图;
  • 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器;
  • 初始化编译器, 存储el对应的dom元素, 存储vm实例, 调用初始化方法
  • 在初始化方法中, 从根节点开始, 取出根节点的所有子节点, 逐个对节点进行解析
  • 解析节点过程中
  • 解析指令存在, 取出绑定值, 替换模板数据, 完成首次视图的初始化
  • 给指令对应的节点绑定更新函数, 并实例化一个订阅器Wather
  • 对于v-model指令, 监听'input'事件,实现视图更新是,去更新模型的数据
//定义解析器
//解析指令,替换模板数据,初始视图
//模板的指令绑定更新函数,数据更新时,更新视图
classCompile{
constructor(el,vm){
this.el=el
this.vm=vm
this.init(this.el)
}

init(el){
this.compileEle(el)
}
compileEle(ele){
constnodes=ele.children
		// 遍历节点进行解析
for(constnodeofnodes){
		 // 如果有子节点,递归调用
if(node.children&&node.children.length!==0){
this.compileEle(node)
}

//指令时v-model并且是标签是输入标签
consthasVmodel=node.hasAttribute('v-model')
constisInputTag=['INPUT','TEXTAREA'].indexOf(node.tagName)!==-1
if(hasVmodel&&isInputTag){
constexp=node.getAttribute('v-model')
constval=this.vm.$data[exp]
constattr='value'
//初次模型值推到视图层,初始化视图
this.modelToView(node,val,attr)
//实例化一个订阅者,将更新函数绑定到订阅者上,未来数据更新,可以更新视图
newWather(this.vm,exp,(newVal)=>{
this.modelToView(node,newVal,attr)
})

//监听视图的改变
node.addEventListener('input',(e)=>{
this.viewToModel(exp,e.target.value)
})
}
		 
		 // 指令时v-bind
if(node.hasAttribute('v-bind')){
constexp=node.getAttribute('v-bind')
constval=this.vm.$data[exp]
constattr='innerHTML'
//初次模型值推到视图层,初始化视图
this.modelToView(node,val,attr)
//实例化一个订阅者,将更新函数绑定到订阅者上,未来数据更新,可以更新视图
newWather(this.vm,exp,(newVal)=>{
this.modelToView(node,newVal,attr)
})
}
}
}
//将模型值更新到视图
modelToView(node,val,attr){
node[attr]=val
}
//将视图值更新到模型上
viewToModel(exp,val){
this.vm.$data[exp]=val
}
}

完整代码

<!DOCTYPEhtml>
<htmllang="en">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width,initial-scale=1.0">
<title>Document</title>
</head>
<body>
<divid="app">
<form>
<inputtype="text"v-model="username">
</form>
<div>
<spanv-bind="username"></span>
</div>
<pv-bind="username"></p>
</div>
<script>

classMyVue{
constructor(options){
this.$options=options
this.$el=document.querySelector(this.$options.el)
this.$data=options.data

//监视数据的属性
this.observable(this.$data)

//编译节点
newCompile(this.$el,this)
}
//递归遍历数据对象的所有属性,进行数据属性的劫持{username:'LastStarDust'}
observable(obj){
//obj为空或者不是对象,不做任何操作
constisEmpty=!obj||typeofobj!=='object'
if(isEmpty){
return
}

//['username']
constkeys=Object.keys(obj)
keys.forEach(key=>{
//如果属性值是对象,递归调用
letval=obj[key]
if(typeofval==='object'){
this.observable(val)
}
//this.defineReactive(this.$data,'username','LastStarDust')
this.defineReactive(obj,key,val)
})

returnobj
}

//数据劫持,修改属性的get和set方法
defineReactive(obj,key,val){
constdep=newDep()
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
//会在初始化时,触发属性get()方法,来到这里Dep.target有值,将其作为订阅者存储起来,在触发属性的set()方法时,调用notify方法
if(Dep.target){
dep.add(Dep.target)
}
console.log(`取出${key}属性值:值为${val}`)
returnval
},
set(newVal){
//没有发生变化,不做更新
if(newVal===val){
return
}
console.log(`更新属性${key}的值为:${newVal}`)

val=newVal
dep.notify()
}
})
}
}

//定义消息订阅器
classDep{
//静态属性Dep.target,这是一个全局唯一的Watcher,因为在同一时间只能有一个全局的Watcher
statictarget=null
constructor(){
//存储订阅者
this.subs=[]
}
//添加订阅者
add(sub){
this.subs.push(sub)
}
//通知
notify(){
this.subs.forEach(sub=>{
//调用订阅者的update方法
sub.update()
})
}
}

//定义订阅者类
classWather{
constructor(vm,exp,cb){
this.vm=vm//vm实例
this.exp=exp//指令对应的字符串值,如v-model="username",exp相当于"username"
this.cb=cb//回到函数更新视图时调用
this.value=this.get()//将自己添加到消息订阅器Dep中
}

get(){
//将当前订阅者作为全局唯一的Wather,添加到Dep.target上
Dep.target=this
//获取数据,触发属性的getter方法
constvalue=this.vm.$data[this.exp]
//在执行添加到消息订阅Dep后,重置Dep.target
Dep.target=null
returnvalue
}

//执行更新
update(){
this.run()
}

run(){
//从Model模型中取出属性值
constnewVal=this.vm.$data[this.exp]
constoldVal=this.value
if(newVal===oldVal){
returnfalse
}
//执行回调函数,将vm实例,新值,旧值传递过去
this.cb.call(this.vm,newVal,oldVal)
}
}

//定义解析器
//解析指令,替换模板数据,初始视图
//模板的指令绑定更新函数,数据更新时,更新视图
classCompile{
constructor(el,vm){
this.el=el
this.vm=vm
this.init(this.el)
}

init(el){
this.compileEle(el)
}
compileEle(ele){
constnodes=ele.children
for(constnodeofnodes){
if(node.children&&node.children.length!==0){
//递归调用,编译子节点
this.compileEle(node)
}

//指令时v-model并且是标签是输入标签
consthasVmodel=node.hasAttribute('v-model')
constisInputTag=['INPUT','TEXTAREA'].indexOf(node.tagName)!==-1
if(hasVmodel&&isInputTag){
constexp=node.getAttribute('v-model')
constval=this.vm.$data[exp]
constattr='value'
//初次模型值推到视图层,初始化视图
this.modelToView(node,val,attr)
//实例化一个订阅者,将更新函数绑定到订阅者上,未来数据更新,可以更新视图
newWather(this.vm,exp,(newVal)=>{
this.modelToView(node,newVal,attr)
})

//监听视图的改变
node.addEventListener('input',(e)=>{
this.viewToModel(exp,e.target.value)
})
}

if(node.hasAttribute('v-bind')){
constexp=node.getAttribute('v-bind')
constval=this.vm.$data[exp]
constattr='innerHTML'
//初次模型值推到视图层,初始化视图
this.modelToView(node,val,attr)
//实例化一个订阅者,将更新函数绑定到订阅者上,未来数据更新,可以更新视图
newWather(this.vm,exp,(newVal)=>{
this.modelToView(node,newVal,attr)
})
}
}
}
//将模型值更新到视图
modelToView(node,val,attr){
node[attr]=val
}
//将视图值更新到模型上
viewToModel(exp,val){
this.vm.$data[exp]=val
}
}

constmyVm=newMyVue({
el:'#app',
data:{
username:'LastStarDust'
}
})

//console.log(Dep.target)
</script>
</body>
</html>

以上就是vue实现简易的双向数据绑定的详细内容,更多关于vue 实现双向数据绑定的资料请关注其它相关文章!

您可能有感兴趣的文章
Vue路由参数的传递与获取方式详细介绍

vue学习记录之动态组件浅析

如何解决ElementUI组件中el-upload上传图片不显示问题

解读element el-upload上传的附件名称不显示 file-list赋值

一篇关于el-table-column的formatter的如何使用及说明