简析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 };