双向数据绑定机理
vue的数据核心是双向数据绑定,其根本机理就是通过object.defineProperty()和发布-订阅模式(观察者模式)进行实现的
object.defineProperty()方法
object.defineProperty()是vue实现数据功能的核心方法。该方法直接在一个对象上定义一个新的属性,或者修改一个已经存在的属性,并且返回这个对象
首先我们要先看一下,object.defineProperty()方法什么时候触发
我们在页面中定义一个span和input元素,在js中进行获取这两个元素,设置一个obj为监听对象,当前我们要定义或者修改的属性内容对象为obj,给obj对象设置对应的方法,此时如果想要触发get或者set方法,必须要对obj.hello属性进行设置或者获取
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 </head> 7 <body> 8 <input type="text" id="text"> 9 <span id="show"></span> 10 </body> 11 <script> 12 var text=document.getElementById("text"); 13 var show=document.getElementById("show"); 14 var obj={}; 15 //定义obj的hello属性 16 Object.defineProperty(obj,'hello',{ 17 // set指的是设置的意思,该方法指的是当obj.hello属性被触发时,会执行set方法,该方法返回被触发的内容,可以设置相应的内容 18 set:(value)=>{ 19 // 在触发set方法的时候我们可以将input输入框和span的value设置得到的value值 20 text.value=value; 21 show.innerHTML=value; 22 console.log('我是set方法',value) 23 24 }, 25 // get指的是设置的意思,该方法指的是当obj.hello属性被触发时候,get方法被执行 26 get:()=>{ 27 console.log('我是get方法') 28 } 29 }) 30 //事件监听 31 document.addEventListener('keyup',(e)=>{ 32 //设置hello属性,该属性会触发set方法 33 obj.hello=e.target.value; 34 //设置hello属性,该属性会触发get方法 35 console.log(obj.hello) 36 }) 37 </script> 38 </html>
此时注意,如果现在输入get方法的值是undefined,因为此时的get方法没有return
需要设置值的话,必须在get方法中有return结果
此时我们修改了一个get方法的获取结果
1 <script> 2 var text=document.getElementById("text"); 3 var show=document.getElementById("show"); 4 var obj={}; 5 var name='' 6 //定义obj的hello属性 7 Object.defineProperty(obj,'hello',{ 8 // set指的是设置的意思,该方法指的是当obj.hello属性被触发时,会执行set方法,该方法返回被触发的内容,可以设置相应的内容 9 set:(value)=>{ 10 // 在触发set方法的时候我们可以将input输入框和span的value设置得到的value值 11 text.value=value; 12 show.innerHTML=value; 13 name=value 14 console.log('我是set方法',value) 15 16 }, 17 // get指的是设置的意思,该方法指的是当obj.hello属性被触发时候,get方法被执行 18 get:()=>{ 19 console.log('我是get方法') 20 return name; 21 } 22 }) 23 //事件监听 24 document.addEventListener('keyup',(e)=>{ 25 //设置hello属性,该属性会触发set方法 26 obj.hello=e.target.value; 27 //设置hello属性,该属性会触发get方法 28 console.log(obj.hello) 29 }) 30 </script>
参数的含义:
Object.defineProperty(obj,prop,des)
obj:需要定义的属性对象
prop: 需要被定义或者修改的属性名
des: 需要被定义或者修改的属性描述(可以是一个对象)
除了以上的三个参数外还有其他的参数
value: 指的是配置相关对象属性的值,可以是任意类型
configurable: 布尔值,如果为true的时候该属性能够被改变,也能被删除,默认为false
看一个案例
1 <script> 2 var obj={ 3 'prop':'prop' 4 } 5 Object.defineProperty(obj,'prop',{ 6 value:'hello' 7 }) 8 console.log(obj) 9 </script>
此时我们可以看到页面有这个带有hello参数的对象
此时我们删除之后我们可以看到删除了hello
delete obj.prop //删除prop参数
我们将configurable值设为false
configurable:false,
enumerable: 布尔值,该属性为true的时候可以出现在对象的枚举值当中,默认为false
1 <script> 2 var obj = {} 3 Object.defineProperty(obj, 'a', { 4 configurable: true, 5 enumerable: true, 6 value: '1' 7 }) 8 Object.defineProperty(obj, 'b', { 9 enumerable: false, 10 configurable: true, 11 value: '2' 12 }) 13 Object.defineProperty(obj, 'c', { 14 configurable: true, 15 value: '3' 16 }) 17 obj.d = 4; 18 console.log(Object.keys(obj)) 19 </script>
writable:布尔值,如果为true的时候该属性才能通过赋值运算法进行改变(=),默认为false
1 <script> 2 var obj = {} 3 Object.defineProperty(obj, 'a', { 4 value: '1' 5 }) 6 Object.defineProperty(obj, 'b', { 7 value: '2' 8 }) 9 Object.defineProperty(obj, 'c', { 10 11 value: '3' 12 }) 13 obj.d = 4; 14 console.log(obj) 15 </script>
此时我们想要修改obj.c 的值
需要填加wirtable属性为true
1 <script> 2 var obj = {} 3 Object.defineProperty(obj, 'a', { 4 value: '1' 5 }) 6 Object.defineProperty(obj, 'b', { 7 value: '2' 8 }) 9 Object.defineProperty(obj, 'c', { 10 writable: true, 11 value: '3' 12 }) 13 obj.c = '5' 14 obj.d = 4; 15 console.log(obj) 16 </script>
此时如果我们不加这个writable属性,直接修改a的值,不会报错也不会修改数据
发布-订阅模式(观察者模式)
我们看下面的一个汇率转换案例
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>Document</title> 8 </head> 9 <body> 10 11 </body> 12 <script> 13 //设置人民币类 14 function RMB(){ 15 //罗列属性 16 this.p=null; 17 this.input=null; 18 //数组管理其他的需要传递数据的外币实例 19 this.arr=[]; 20 //初始化 21 this.init(); 22 //设置事件监听 23 this.bindEvent() 24 } 25 RMB.prototype.init=function(){ 26 //创建节点 27 this.p=document.createElement("p"); 28 this.p.innerHTML='人民币:'; 29 //节点上树 30 document.body.appendChild(this.p); 31 //初始化input 32 this.input=document.createElement("input"); 33 this.p.appendChild(this.input) 34 } 35 //注册的方法,管理外币的实例 36 RMB.prototype.reg=function(w){ 37 this.arr.push(w) 38 } 39 //原型对象的一个事件 40 RMB.prototype.bindEvent=function(){ 41 //维护发布内容,当前的输入框如果输入了内容,此时需要需要给对应的订阅者发布内容 42 var self=this; 43 self.input.oninput=function(){ 44 for(let i=0;i< self.arr.length;i++){ 45 self.arr[i].change(self.input.value) 46 } 47 console.log(self.input.value) 48 } 49 } 50 var rmb=new RMB() 51 52 //订阅者类 53 function WB(name,rate){ 54 //属性的罗列 55 this.p=null; 56 this.input=null; 57 this.name=name; 58 this.rate=rate; 59 //初始化 60 this.init() 61 // 进行订阅 62 rmb.reg(this) 63 } 64 WB.prototype.init=function(){ 65 //创建节点 66 this.p=document.createElement("p"); 67 this.p.innerHTML=this.name+':'; 68 //节点上树 69 document.body.appendChild(this.p); 70 //初始化input 71 this.input=document.createElement("input"); 72 this.p.appendChild(this.input) 73 } 74 //订阅者类设置一个change方法,目的就是接收发布者发布的内容 75 WB.prototype.change=function(e){ 76 this.input.value=e*this.rate 77 } 78 var wb=new WB('美元',6) 79 var wb=new WB('英镑',9) 80 </script>
双向数据绑定实现过程
我们已经知道了数据的双向绑定首先要通过对数据的劫持,所以首先我们需要设置一个监听器Observer,用来监听所有的属性,如果属性发生了变化,就需要告诉订阅这Watcher看是否需要更新。因为订阅者有多个,所以我们需要有个消息订阅中心Dep专门收集这些订阅者,然后在监听器和订阅者之间进行统一管理
1>实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者
2>实现一个订阅者Watcher,可以收到属性的变化并且通知相应的函数,从而实现视图的更新
1.实现Observer
1 <script> 2 function defineReactive(data, key, val) { 3 // 通过递归的方式对所有的属性进行实时监听 4 observe(val) 5 var dep=new Dep() 6 Object.defineProperty(data, key, { 7 enumerable: true, 8 configurable: true, 9 get: function() { 10 return val 11 //在这里判断是否添加订阅者 12 if(Dep.target){ 13 dep.addSub(Dep.target) 14 } 15 }, 16 set: function(newVal) { 17 val = newVal 18 //当数据变化的时候通知所有的订阅者 19 dep.notify(); 20 } 21 }) 22 } 23 24 function observe(data) { 25 if (!data || typeof data !== 'object') { 26 return 27 } 28 // 实现一个递归 29 Object.keys(data).forEach(function(key) { 30 defineReactive(data, key, data[key]) 31 }) 32 } 33 34 var library = { 35 book1: { 36 name: '' 37 }, 38 book2: '' 39 } 40 // 将library所有的属性进行监听,也就是数据的劫持 41 observe(library) 42 //设计思路发布订阅 43 function Dep(){ 44 //维护所有的被发布的实例对象 45 this.subs=[]; 46 } 47 Dep.prototype.addSub=function(sub){ 48 //添加被发布的对象实例方法 49 this.subs.push(sub); 50 } 51 Dep.prototype.notify=function(sub){ 52 this.subs.forEach(function(sub){ 53 this.update() 54 }) 55 } 56 Dep.target=null; 57 //设置watcher 58 function Watcher(vm,exp,callback){ 59 this.callback=callback; 60 this.vm=vm; 61 this.exp=exp; 62 this.value=this.get(); 63 } 64 Watcher.prototype.get=function(){ 65 //taget指向当前的this 66 Dep.target=this; 67 var value=this.vm.data[this.exp]; 68 return value; 69 //释放target 70 Dep.target=null; 71 } 72 //更新数据的状态 73 Watcher.prototype.update=function(){ 74 this.run() 75 } 76 Watcher.prototype.run=function(){ 77 //新的value 78 var value=this.vm.data[this.exp]; 79 //当前this.value的状态 80 var oldVal=this.value; 81 //判断当前的value和新的value是否一直,如果不一致,就更新value的状态 82 if(this.value!==lidVal){ 83 this.value=value; 84 this.callback(this.vm,value,oldVal) 85 } 86 } 87 //将observer和watcher关联起来 88 function SelVue(data,el,exp){ 89 this.data=data; 90 //监听数据进行属性的监听 91 observe(data); 92 el.innerHTML=this.data[exp]; 93 new Watcher(this,exp,function(value){ 94 el.innerHTML=value 95 }) 96 } 97 var el=document.getElementById("name") 98 var selVue=new SelVue({ 99 name:'小明', 100 },el,'name') 101 window.setTimeout(function(){ 102 selVue.data.name='小红' 103 console.log('修改了name属性值为'+selVue.data.name) 104 },2000) 105 </script>
此时等2秒后可以看到