IT序号网

Vue的双向数据绑定

xmjava 2021年05月25日 编程语言 436 0

1、原理

Vue的双向数据绑定的原理相信大家也都十分了解了,主要是通过 Object对象的defineProperty属性,重写data的set和get函数来实现的,这里对原理不做过多描述,主要还是来实现一个实例。为了使代码更加的清晰,这里只会实现最基本的内容,主要实现v-model,v-bind 和v-click三个命令,其他命令也可以自行补充。

添加网上的一张图

2、实现

页面结构很简单,如下

1 <div id="app"> 
2     <form> 
3       <input type="text"  v-model="number"> 
4       <button type="button" v-click="increment">增加</button> 
5     </form> 
6     <h3 v-bind="number"></h3> 
7   </div>

包含:

 1. 一个input,使用v-model指令 
 2. 一个button,使用v-click指令 
 3. 一个h3,使用v-bind指令。

我们最后会通过类似于vue的方式来使用我们的双向数据绑定,结合我们的数据结构添加注释
 1 var app = new myVue({ 
 2       el:'#app', 
 3       data: { 
 4         number: 0 
 5       }, 
 6       methods: { 
 7         increment: function() { 
 8           this.number ++; 
 9         }, 
10       } 
11     })

首先我们需要定义一个myVue构造函数:

1 function myVue(options) { 
2    
3 }

为了初始化这个构造函数,给它添加一 个_init属性

1 function myVue(options) { 
2   this._init(options); 
3 } 
4 myVue.prototype._init = function (options) { 
5     this.$options = options;  // options 为上面使用时传入的结构体,包括el,data,methods 
6     this.$el = document.querySelector(options.el); // el是 #app, this.$el是id为app的Element元素 
7     this.$data = options.data; // this.$data = {number: 0} 
8     this.$methods = options.methods;  // this.$methods = {increment: function(){}} 
9   }

接下来实现_obverse函数,对data进行处理,重写data的set和get函数

并改造_init函数

 1  myVue.prototype._obverse = function (obj) { // obj = {number: 0} 
 2     var value; 
 3     for (key in obj) {  //遍历obj对象 
 4       if (obj.hasOwnProperty(key)) { 
 5         value = obj[key];  
 6         if (typeof value === 'object') {  //如果值还是对象,则遍历处理 
 7           this._obverse(value); 
 8         } 
 9         Object.defineProperty(this.$data, key, {  //关键 
10           enumerable: true, 
11           configurable: true, 
12           get: function () { 
13             console.log(`获取${value}`); 
14             return value; 
15           }, 
16           set: function (newVal) { 
17             console.log(`更新${newVal}`); 
18             if (value !== newVal) { 
19               value = newVal; 
20             } 
21           } 
22         }) 
23       } 
24     } 
25   } 
26   
27  myVue.prototype._init = function (options) { 
28     this.$options = options; 
29     this.$el = document.querySelector(options.el); 
30     this.$data = options.data; 
31     this.$methods = options.methods; 
32     
33     this._obverse(this.$data); 
34   }

接下来我们写一个指令类Watcher,用来绑定更新函数,实现对DOM元素的更新

 1 function Watcher(name, el, vm, exp, attr) { 
 2     this.name = name;         //指令名称,例如文本节点,该值设为"text" 
 3     this.el = el;             //指令对应的DOM元素 
 4     this.vm = vm;             //指令所属myVue实例 
 5     this.exp = exp;           //指令对应的值,本例如"number" 
 6     this.attr = attr;         //绑定的属性值,本例为"innerHTML" 
 7  
 8     this.update(); 
 9   } 
10  
11   Watcher.prototype.update = function () { 
12     this.el[this.attr] = this.vm.$data[this.exp]; //比如 H3.innerHTML = this.data.number; 当number改变时,会触发这个update函数,保证对应的DOM内容进行了更新。 
13   }

更新_init函数以及_obverse函数

 1 myVue.prototype._init = function (options) { 
 2     //... 
 3     this._binding = {};   //_binding保存着model与view的映射关系,也就是我们前面定义的Watcher的实例。当model改变时,我们会触发其中的指令类更新,保证view也能实时更新 
 4     //... 
 5   } 
 6   
 7   myVue.prototype._obverse = function (obj) { 
 8     //... 
 9       if (obj.hasOwnProperty(key)) { 
10         this._binding[key] = {    // 按照前面的数据,_binding = {number: _directives: []}                                                                                                                                                   
11           _directives: [] 
12         }; 
13         //... 
14         var binding = this._binding[key]; 
15         Object.defineProperty(this.$data, key, { 
16           //... 
17           set: function (newVal) { 
18             console.log(`更新${newVal}`); 
19             if (value !== newVal) { 
20               value = newVal; 
21               binding._directives.forEach(function (item) {  // 当number改变时,触发_binding[number]._directives 中的绑定的Watcher类的更新 
22                 item.update(); 
23               }) 
24             } 
25           } 
26         }) 
27       } 
28     } 
29   }

那么如何将view与model进行绑定呢?接下来我们定义一个_compile函数,用来解析我们的指令(v-bind,v-model,v-clickde)等,并在这个过程中对view与model进行绑定。

 1  myVue.prototype._init = function (options) { 
 2    //... 
 3     this._complie(this.$el); 
 4   } 
 5   
 6 myVue.prototype._complie = function (root) { root 为 id为app的Element元素,也就是我们的根元素 
 7     var _this = this; 
 8     var nodes = root.children; 
 9     for (var i = 0; i < nodes.length; i++) { 
10       var node = nodes[i]; 
11       if (node.children.length) {  // 对所有元素进行遍历,并进行处理 
12         this._complie(node); 
13       } 
14  
15       if (node.hasAttribute('v-click')) {  // 如果有v-click属性,我们监听它的onclick事件,触发increment事件,即number++ 
16         node.onclick = (function () { 
17           var attrVal = nodes[i].getAttribute('v-click'); 
18           return _this.$methods[attrVal].bind(_this.$data);  //bind是使data的作用域与method函数的作用域保持一致 
19         })(); 
20       } 
21  
22       if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) { // 如果有v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件 
23         node.addEventListener('input', (function(key) {   
24           var attrVal = node.getAttribute('v-model'); 
25            //_this._binding['number']._directives = [一个Watcher实例] 
26            // 其中Watcher.prototype.update = function () { 
27            //    node['vaule'] = _this.$data['number'];  这就将node的值保持与number一致 
28            // } 
29           _this._binding[attrVal]._directives.push(new Watcher(   
30             'input', 
31             node, 
32             _this, 
33             attrVal, 
34             'value' 
35           )) 
36  
37           return function() { 
38             _this.$data[attrVal] =  nodes[key].value; // 使number 的值与 node的value保持一致,已经实现了双向绑定 
39           } 
40         })(i)); 
41       }  
42  
43       if (node.hasAttribute('v-bind')) { // 如果有v-bind属性,我们只要使node的值及时更新为data中number的值即可 
44         var attrVal = node.getAttribute('v-bind'); 
45         _this._binding[attrVal]._directives.push(new Watcher( 
46           'text', 
47           node, 
48           _this, 
49           attrVal, 
50           'innerHTML' 
51         )) 
52       } 
53     } 
54   }

至此,我们已经实现了一个简单vue的双向绑定功能,包括v-bind, v-model, v-click三个指令。效果如下图

附上全部代码,不到150行

  1 <!DOCTYPE html> 
  2 <head> 
  3   <title>myVue</title> 
  4 </head> 
  5 <style> 
  6   #app { 
  7     text-align: center; 
  8   } 
  9 </style> 
 10 <body> 
 11   <div id="app"> 
 12     <form> 
 13       <input type="text"  v-model="number"> 
 14       <button type="button" v-click="increment">增加</button> 
 15     </form> 
 16     <h3 v-bind="number"></h3> 
 17   </div> 
 18 </body> 
 19  
 20 <script> 
 21   function myVue(options) { 
 22     this._init(options); 
 23   } 
 24  
 25   myVue.prototype._init = function (options) { 
 26     this.$options = options; 
 27     this.$el = document.querySelector(options.el); 
 28     this.$data = options.data; 
 29     this.$methods = options.methods; 
 30  
 31     this._binding = {}; 
 32     this._obverse(this.$data); 
 33     this._complie(this.$el); 
 34   } 
 35   
 36   myVue.prototype._obverse = function (obj) { 
 37     var value; 
 38     for (key in obj) { 
 39       if (obj.hasOwnProperty(key)) { 
 40         this._binding[key] = {                                                                                                                                                           
 41           _directives: [] 
 42         }; 
 43         value = obj[key]; 
 44         if (typeof value === 'object') { 
 45           this._obverse(value); 
 46         } 
 47         var binding = this._binding[key]; 
 48         Object.defineProperty(this.$data, key, { 
 49           enumerable: true, 
 50           configurable: true, 
 51           get: function () { 
 52             console.log(`获取${value}`); 
 53             return value; 
 54           }, 
 55           set: function (newVal) { 
 56             console.log(`更新${newVal}`); 
 57             if (value !== newVal) { 
 58               value = newVal; 
 59               binding._directives.forEach(function (item) { 
 60                 item.update(); 
 61               }) 
 62             } 
 63           } 
 64         }) 
 65       } 
 66     } 
 67   } 
 68  
 69   myVue.prototype._complie = function (root) { 
 70     var _this = this; 
 71     var nodes = root.children; 
 72     for (var i = 0; i < nodes.length; i++) { 
 73       var node = nodes[i]; 
 74       if (node.children.length) { 
 75         this._complie(node); 
 76       } 
 77  
 78       if (node.hasAttribute('v-click')) { 
 79         node.onclick = (function () { 
 80           var attrVal = nodes[i].getAttribute('v-click'); 
 81           return _this.$methods[attrVal].bind(_this.$data); 
 82         })(); 
 83       } 
 84  
 85       if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) { 
 86         node.addEventListener('input', (function(key) { 
 87           var attrVal = node.getAttribute('v-model'); 
 88           _this._binding[attrVal]._directives.push(new Watcher( 
 89             'input', 
 90             node, 
 91             _this, 
 92             attrVal, 
 93             'value' 
 94           )) 
 95  
 96           return function() { 
 97             _this.$data[attrVal] =  nodes[key].value; 
 98           } 
 99         })(i)); 
100       }  
101  
102       if (node.hasAttribute('v-bind')) { 
103         var attrVal = node.getAttribute('v-bind'); 
104         _this._binding[attrVal]._directives.push(new Watcher( 
105           'text', 
106           node, 
107           _this, 
108           attrVal, 
109           'innerHTML' 
110         )) 
111       } 
112     } 
113   } 
114  
115   function Watcher(name, el, vm, exp, attr) { 
116     this.name = name;         //指令名称,例如文本节点,该值设为"text" 
117     this.el = el;             //指令对应的DOM元素 
118     this.vm = vm;             //指令所属myVue实例 
119     this.exp = exp;           //指令对应的值,本例如"number" 
120     this.attr = attr;         //绑定的属性值,本例为"innerHTML" 
121  
122     this.update(); 
123   } 
124  
125   Watcher.prototype.update = function () { 
126     this.el[this.attr] = this.vm.$data[this.exp]; 
127   } 
128  
129   window.onload = function() { 
130     var app = new myVue({ 
131       el:'#app', 
132       data: { 
133         number: 0 
134       }, 
135       methods: { 
136         increment: function() { 
137           this.number ++; 
138         }, 
139       } 
140     }) 
141   } 
142 </script>

转自https://segmentfault.com/a/1190000014274840


评论关闭
IT序号网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!