简析Vue的响应式(或叫双向数据绑定)及其原理

        上一讲,我们讲Vue和React异同的时候,讲到Vue是响应式的,那么这个响应式到底具体是什么样的?这一讲,我们来仔细看看这个话题。

        简单点来说,就是在Vue的数据变量值变化时,变化可以同步到视图;在视图值变化时,视图的值变化可以同步到Vue的数据变量(注意:这里的数据变量是指Vue实例的data属性中的返回值对象的属性,视图值是指表单元素的输入值变化)。

 

        我们又把满足前半句的叫单向数据绑定(即数据到视图的绑定),把同时满足前半句和后半句的叫双向数据绑定(即数据到视图的绑定和视图到数据的绑定)。那么这两个绑定是如何实现的呢?或者说这两个数据绑定的实现原理是什么?

        其其实也很简单。前半句的实现是采用了一个叫观察者模式的设计方法来实现的,后半句是直接采用js自带是事件机制来实现的。下面我们来分别阐述下这两个实现。

 

       1.通过事件机制来实现:即通过对视图上来自用户的输入进行监听,在监听到新的数据变化时将其更新到Vue实例的返回值对象的属性上,这就实现了视图到数据的绑定。待用户真正输入数据时,就会触发视图到数据的更新。是不是很简单?嗯....反正我觉得是很简单。

       2.通过观察者模式来实现:即主要通过两个对象来实现,观察者对象目标对象。由于这个实现过程比较负责,我们先简述下这两个对象的方法和属性,后面再将这两个对象的方法和属性的调用纳入Vue整个编译观察过程来综述下。

(1)对象简述:

        观察者对象:目标对象有一个更新方法。其用来在获得目标对象变化时,更新变化。

        目标对象:目标对象有一个通知方法及一个数组变量属性。数组变量属性用来存储所有观察该目标对象的观察者对象;通知方法用来通知所有观察者。

 

(2)纳入Vue整个编译观察过程综述:

   上面我们看完了这两个对象的属性和方法。然后我们来具体描述下这个过程在Vue运行过程中的调用。我们知道,我们在写Vue的时候会写一些包含data、mounted、method、el等等属性的配置对象给Vue构造器,这样Vue构造器会在Vue实例化的时候,先根据我们传给它的data属性,利用Object对象的defineProperty方法对其data对象进行递归监听每一个属性的改变和调用。这里每个属性即是我们上面说的目标对象,在这个属性值被改变时,其会调用它的通知方法,通知对它进行观察的观察列表里面的所有观察者。然后在Vue对data属性进行递归监听完成以后,再对传给它的模板进行编译,在编译时碰到双括号、指令等等Vue基于原生html添加的内容时,Vue会把其解析成一个个观察者,然后将其存储到观察的相应目标属性的观察列表中,这样就完成了数据到视图的绑定。最后,待我们调用方法对目标属性进行更改时,目标对象通知自己观察者列表里面的所有观察者更新它们的视图,这就完成了数据到视图的更新。

 

      至此,我们就介绍完了Vue的双向绑定或者叫数据响应。是不是很简单?

 

  最后,把实现的具体代码摘录如下,有兴趣的同学可以具体看看:

       0.index.html

 1 <!DOCTYPE html>
 2 <html>
 3   <head>
 4     <title>vue的响应式</title>
 5     <meta charset="utf-8" />
 6     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 7     <meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
 8   </head>
 9   <body>
10     <div id="app">
11       <h2>{{title}}</h2>
12       <input v-model="name">
13       <h1>{{name}}</h1>
14       <button v-on:click="clickMe">click me!</button>
15     </div>
16     <script type="text/javascript" src="./index.js"></script>
17   </body>
18 </html>

 

       1.index.js

 1 import SelfVue from './vue';
 2 
 3 new SelfVue({
 4   el: '#app',
 5   data(){
 6     return {
 7       title: 'hello world',
 8       name: 'canfoo'
 9     }
10   },
11   methods: {
12     clickMe: function () {
13       this.title = 'hello world';
14     }
15   },
16   mounted: function () {
17     window.setTimeout(() => {
18         this.title = '你好';
19     }, 1000);
20   }
21 });

2.vue.js

 1 import { observe } from './observe';
 2 import Compile from './compile';
 3 
 4 function SelfVue (options) {
 5   var _self = this;
 6   this.data = options.data();
 7   this.methods = options.methods;
 8 
 9   Object.keys(this.data).forEach(function(key) {
10     _self.proxyKeys(key);
11   });
12 
13   observe(this.data);
14   new Compile(options.el, this);
15   options.mounted.call(this); 
16 }
17 
18 SelfVue.prototype = {
19   proxyKeys: function (key) {
20     var self = this;
21     Object.defineProperty(this, key, {
22       enumerable: false,
23       configurable: true,
24       get: function proxyGetter() {
25         return self.data[key];
26       },
27       set: function proxySetter(newVal) {
28         self.data[key] = newVal;
29       }
30     });
31   }
32 }

3.observe.js

 1 import Dep from './dep'
 2 import Deps from './deps';
 3 
 4 var tempKey = '';
 5 export function defineReactive(data, key, val) {
 6   tempKey = tempKey ? `${tempKey}.${key}` : key;
 7   observe(val); 
 8   var dep = new Dep();
 9   Deps[tempKey] = dep;
10   tempKey = '';
11    
12   Object.defineProperty(data, key, {
13       enumerable: true,
14       configurable: true,
15       get: function(){
16         return val;
17       },
18       set: function(newVal) {
19         if (val === newVal) {
20           return;
21         }
22         val = newVal;
23         console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
24         dep.notify(); 
25       }
26   });
27 }
28  
29 export function observe(data) {
30   if (!data || typeof data !== 'object') {
31       return;
32   }
33   Object.keys(data).forEach(function(key) {
34       defineReactive(data, key, data[key]);
35   });
36 };

 

4.compile.js

  1 import Watcher from './watcher'
  2 import Deps from './deps';
  3 
  4 function Compile(el, vm) {
  5     this.vm = vm;
  6     this.el = document.querySelector(el);
  7     this.fragment = null;
  8     this.init();
  9 }
 10 
 11 Compile.prototype = {
 12   init: function () {
 13     if(this.el) {
 14       this.fragment = this.nodeToFragment(this.el);// ge
 15       this.compileElement(this.fragment);
 16       this.el.appendChild(this.fragment);
 17     }else {
 18       console.log('Dom元素不存在');
 19     }
 20   },
 21   nodeToFragment: function (el) {
 22     var fragment = document.createDocumentFragment();
 23     var child = el.firstChild;
 24     while (child) {
 25       // 将Dom元素移入fragment中
 26       fragment.appendChild(child);
 27       child = el.firstChild
 28     }
 29     return fragment;
 30   },
 31   compileElement: function (el) {
 32     var childNodes = el.childNodes;
 33     var self = this;
 34 
 35     [].slice.call(childNodes).forEach(function(node) {
 36       var reg = /\{\{(.*)\}\}/;
 37       var text = node.textContent;
 38 
 39       if (self.isElementNode(node)) {  
 40         self.compile(node);
 41       } else if (self.isTextNode(node) && reg.test(text)) {
 42         self.compileText(node, reg.exec(text)[1]);
 43       }
 44 
 45       if(node.childNodes && node.childNodes.length) {
 46           self.compileElement(node);  
 47       }
 48     });
 49   },
 50   compile: function(node) {
 51     var nodeAttrs = node.attributes;
 52     var self = this;
 53     Array.prototype.forEach.call(nodeAttrs, function(attr) {
 54       var attrName = attr.name;
 55       if (self.isDirective(attrName)) {
 56         var exp = attr.value;
 57         var dir = attrName.substring(2);
 58         if (self.isEventDirective(dir)) {  
 59           self.compileEvent(node, self.vm, exp, dir);
 60         }else{  //
 61           self.compileModel(node, self.vm, exp, dir);
 62         }
 63         node.removeAttribute(attrName);
 64       }
 65     });
 66   },
 67   compileText: function(node, exp) {
 68     var self = this;
 69     var initText = this.vm[exp];
 70     this.updateText(node, initText);  
 71     Deps[exp].addSub(new Watcher(this.vm, exp, function (value) {
 72         self.updateText(node, value)
 73     }));
 74   },
 75   compileEvent: function (node, vm, exp, dir) {
 76       var eventType = dir.split(':')[1];
 77       var cb = vm.methods && vm.methods[exp];
 78 
 79       if (eventType && cb) {
 80           node.addEventListener(eventType, cb.bind(vm), false);
 81       }
 82   },
 83   compileModel: function (node, vm, exp, dir) {
 84       var self = this;
 85       var val = this.vm[exp];
 86       this.modelUpdater(node, val);
 87       new Watcher(this.vm, exp, function (value) {
 88           self.modelUpdater(node, value);
 89       });
 90 
 91       node.addEventListener('input', function(e) {
 92           var newValue = e.target.value;
 93           if (val === newValue) {
 94               return;
 95           }
 96           self.vm[exp] = newValue;
 97           val = newValue;
 98       });
 99   },
100   updateText: function (node, value) {
101     node.textContent = typeof value == 'undefined' ? '' : value;
102   },
103   isDirective: function(attr) {
104     return attr.indexOf('v-') == 0;
105   },
106   isEventDirective: function(dir) {
107     return dir.indexOf('on:') === 0;
108   },
109   isElementNode: function (node) {
110     return node.nodeType == 1;
111   },
112   isTextNode: function(node) {
113     return node.nodeType == 3;
114   }
115 }

 

5.deps.js

1 export default {
2     
3 }

 

6.dep.js

 1 function Dep () {
 2     this.subs = [];
 3 }
 4 Dep.prototype = {
 5     addSub: function(sub) {
 6       this.subs.push(sub);
 7     },
 8     notify: function() {
 9       this.subs.forEach(function(sub) {
10           sub.update();
11       });
12     }
13 };

7.watcher.js

 1 function Watcher(vm, exp, cb) {
 2   this.cb = cb; 
 3   this.vm = vm;
 4   this.exp = exp; 
 5   this.value = this.get();
 6 }
 7  
 8 Watcher.prototype = {
 9   update: function(){
10     this.run();
11   },
12   run: function() {
13     var value = this.vm.data[this.exp];
14     var oldVal = this.value;
15     if(value !== oldVal) {
16       this.value = value;
17       this.cb.call(this.vm, value, oldVal);
18     }
19   },
20   get: function() {
21       Dep.target = this;
22       var value = this.vm.data[this.exp]  
23       Dep.target = null; 
24       return value;
25   }
26 };

 

 

posted @ 2023-09-01 10:06  浮萍人生  阅读(325)  评论(0编辑  收藏  举报