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