双向数据绑定机理

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秒后可以看到

 

posted @ 2021-10-06 15:58  keyeking  阅读(154)  评论(0编辑  收藏  举报