夏日的草原,早晨空气格外清新,我缠着父亲在草原上漫步。幽幽的草香迎面拂来,红艳艳。朝阳正从地平线上冉冉升起,为辽阔的草原镀上一层金色。草叶上的露珠,像镶在翡翠上的珍珠,闪着五颜六色的光华。我看到草丛中夹着许多粉红色、白色、黄色或是蓝色的不知名的花,把草原装扮得十分美丽。
主要是通过数据劫持和发布订阅一起实现的
- 双向数据绑定 数据更新时,可以更新视图 视图的数据更新是,可以反向更新模型
组成说明
- 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 实现双向数据绑定的资料请关注其它相关文章!