vue 之 双向绑定原理
一、实现双向绑定
详细版:
前端MVVM实现双向数据绑定的做法大致有如下三种:
1.发布者-订阅者模式(backbone.js)
思路:使用自定义的data属性在HTML代码中指明绑定。所有绑定起来的JavaScript对象以及DOM元素都将“订阅”一个发布者对象。任何时候如果JavaScript对象或者一个HTML输入字段被侦测到发生了变化,我们将代理事件到发布者-订阅者模式,这会反过来将变化广播并传播到所有绑定的对象和元素。
vueJS 的思路流程:发布者dep发出通知 => 主题对象subs收到通知并推送给订阅者 => 订阅者watcher执行相应操作
2.脏值检查(angular.js)
思路:angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过
setInterval()
定时轮询检测数据变动,angular只有在指定的事件触发时进入脏值检测,大致如下:
DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
XHR响应事件 ( $http )
浏览器Location变更事件 ( $location )
Timer事件( $timeout , $interval )
执行 $digest() 或 $apply()
3.数据劫持(Vue.js)
思路: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过
Object.defineProperty()
来劫持各个属性的setter
,getter
,在数据变动时发布消息给订阅者,触发相应的监听回调。
Object.defineProperty
作用定义:直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
Object.defineProperty(obj, prop, descriptor)
参数 obj => 要在其上定义属性的对象;
prop => 要定义或修改的属性的名称;
descriptor => 将被定义或修改的属性描述符。
属性描述符 => 数据描述符和存取描述符,两者取一
数据描述符: 具有值的属性
存取描述符: 由getter-setter函数对描述的属性
具有的属性:
注:configurable 可配置性相当于属性的总开关,只有为true时才能设置,而且不可逆
enumerable 是否可枚举,为false时for..in以及Object.keys()将不能枚举出该属性
writable 是否可写,为false时将不能够修改属性的值
get 一个给属性提供 getter 的方法
set 一个给属性提供 setter 的方法
返回值 被传递给函数的对象obj。
示例:
var obj = {}; Object.defineProperty(obj, 'hello', { get: function() { console.log('get val:'+ val); return val; }, set: function(newVal) { val = newVal; console.log('set val:'+ val); } }); obj.hello; // 触发 getter =>get val:undefined obj.hello='111'; // 触发 setter =>set val:111 obj.hello; // 触发 getter =>get val:111
vue 实现双向绑定
实现mvvm的双向绑定的步骤:
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm入口函数,整合以上三者
流程图:
流程解析:
从图中可以看出,当执行 new Vue() 时,Vue 就进入了初始化阶段,一方面Vue 会遍历 data 选项中的属性,并用 Object.defineProperty 将它们转为 getter/setter,实现数据变化监听功能;另一方面,Vue 的指令编译器Compile 对元素节点的指令进行解析,初始化视图,并订阅Watcher 来更新视图, 此时Wather 会将自己添加到消息订阅器中(Dep),初始化完毕。当数据发生变化时,Observer 中的 setter 方法被触发,setter 会立即调用Dep.notify(),Dep 开始遍历所有的订阅者,并调用订阅者的 update 方法,订阅者收到通知后对视图进行相应的更新。因为VUE使用Object.defineProperty方法来做数据绑定,而这个方法又无法通过兼容性处理,所以Vue 不支持 IE8 以及更低版本浏览器。另外,查看vue原代码,发现在vue初始化实例时, 有一个proxy代理方法,它的作用就是遍历data中的属性,把它代理到vm的实例上,这也就是我们可以这样调用属性:vm.a等于vm.data.a。
Observer
利用Obeject.defineProperty()
来监听属性变动,将需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter
和getter
当给这个对象的某个值赋值,就会触发setter
,进而监听到数据变化
监听到变化之后通知订阅者,需要实现一个消息订阅器Dep,通过维护一个数组subs,用来收集订阅者,数据变动触发notify,再调用订阅者的update方法
流程图:
完整代码:
1 function Observer(data) { 2 this.data = data; 3 this.walk(data); 4 } 5 6 Observer.prototype = { 7 walk: function(data) { 8 var me = this; 9 Object.keys(data).forEach(function(key) { 10 me.convert(key, data[key]); 11 }); 12 }, 13 convert: function(key, val) { 14 this.defineReactive(this.data, key, val); 15 }, 16 17 defineReactive: function(data, key, val) { 18 var dep = new Dep(); 19 var childObj = observe(val); 20 21 Object.defineProperty(data, key, { 22 enumerable: true, // 可枚举 23 configurable: false, // 不能再define 24 get: function() { 25 // 添加订阅者watcher到主题对象Dep 26 if (Dep.target) { 27 dep.depend(); 28 } 29 return val; 30 }, 31 set: function(newVal) { 32 if (newVal === val) { 33 return; 34 } 35 val = newVal; 36 // 新的值是object的话,进行监听 37 childObj = observe(newVal); 38 // 通知订阅者 39 dep.notify(); 40 } 41 }); 42 } 43 }; 44 45 function observe(value, vm) { 46 if (!value || typeof value !== 'object') { 47 return; 48 } 49 50 return new Observer(value); 51 }; 52 53 54 var uid = 0; 55 56 function Dep() { 57 this.id = uid++; 58 this.subs = []; 59 } 60 61 Dep.prototype = { 62 addSub: function(sub) { 63 this.subs.push(sub); 64 }, 65 66 depend: function() { 67 Dep.target.addDep(this); 68 }, 69 70 removeSub: function(sub) { 71 var index = this.subs.indexOf(sub); 72 if (index != -1) { 73 this.subs.splice(index, 1); 74 } 75 }, 76 77 notify: function() { 78 this.subs.forEach(function(sub) { 79 sub.update(); 80 }); 81 } 82 }; 83 84 Dep.target = null;
Watcher
Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、定义一个update()方法
3、在Observe中,待属性变动触发dep.notice()发出通知,调用watcher实例自身的update()方法,并触发Compile中绑定的回调
完整代码:
1 function Watcher(vm, expOrFn, cb) { 2 this.cb = cb; // 回调函数 3 this.vm = vm; // this 调用对象 4 this.expOrFn = expOrFn; // watch的对象的key 5 this.depIds = {}; 6 console.log(typeof expOrFn, expOrFn); 7 if (typeof expOrFn === 'function') { // function 8 this.getter = expOrFn; 9 } else { // express 10 // this.getter 等于 this.parseGetter 的return返回的匿名函数 11 this.getter = this.parseGetter(expOrFn); 12 } 13 // 调用get方法,从而触发getter 14 // this.get() ==> this.getter.call(this.vm, this.vm) ==> this.parseGetter(expOrFn) 15 // this.value = parseGetter中return匿名函数的返回值 16 this.value = this.get(); 17 } 18 19 Watcher.prototype = { 20 update: function() { 21 this.run(); // 属性值变化收到通知,每次data属性值变化触发dep.notify() 22 }, 23 run: function() { 24 var value = this.get(); // 取到最新值 25 var oldVal = this.value; 26 if (value !== oldVal) { // 新值与旧值比较 27 this.value = value; 28 this.cb.call(this.vm, value, oldVal); // 执行Compile中绑定的回调,更新视图 29 } 30 }, 31 addDep: function(dep) { 32 // 1. 每次调用run()的时候会触发相应属性的getter 33 // getter里面会触发dep.depend(),继而触发这里的addDep 34 // 2. 假如相应属性的dep.id已经在当前watcher的depIds里,说明不是一个新的属性,仅仅是改变了其值而已 35 // 则不需要将当前watcher添加到该属性的dep里 36 // 3. 假如相应属性是新的属性,则将当前watcher添加到新属性的dep里 37 // 如通过 vm.child = {name: 'a'} 改变了 child.name 的值,child.name 就是个新属性 38 // 则需要将当前watcher(child.name)加入到新的 child.name 的dep里 39 // 因为此时 child.name 是个新值,之前的 setter、dep 都已经失效,如果不把 watcher 加入到新的 child.name 的dep中 40 // 通过 child.name = xxx 赋值的时候,对应的 watcher 就收不到通知,等于失效了 41 // 4. 每个子属性的watcher在添加到子属性的dep的同时,也会添加到父属性的dep 42 // 监听子属性的同时监听父属性的变更,这样,父属性改变时,子属性的watcher也能收到通知进行update 43 // 这一步是在 this.get() --> this.getVMVal() 里面完成,forEach时会从父级开始取值,间接调用了它的getter 44 // 触发了addDep(), 在整个forEach过程,当前wacher都会加入到每个父级过程属性的dep 45 // 例如:当前watcher的是'child.child.name', 那么child, child.child, child.child.name这三个属性的dep都会加入当前watcher 46 if (!this.depIds.hasOwnProperty(dep.id)) { 47 dep.addSub(this); 48 this.depIds[dep.id] = dep; 49 } 50 }, 51 get: function() { 52 Dep.target = this; // 将当前订阅者指向自己 53 console.log(Dep.target); 54 var value = this.getter.call(this.vm, this.vm); // 触发getter,添加自己到属性订阅器中 55 Dep.target = null; // 添加完毕,重置 56 console.log(Dep.target); 57 return value; 58 }, 59 60 parseGetter: function(exp) { 61 if (/[^\w.$]/.test(exp)) return; 62 63 var exps = exp.split('.'); 64 // this.getter.call(this.vm, this.vm)的第二个this.vm 传入 obj 65 return function(obj) { 66 for (var i = 0, len = exps.length; i < len; i++) { 67 if (!obj) return; 68 obj = obj[exps[i]]; 69 } 70 return obj; 71 } 72 } 73 };
实例化Watcher
的时候,调用get()
方法,通过Dep.target = watcher实例
标记订阅者是当前watcher实例,强行触发属性定义的getter
方法,getter
方法执行的时候,就会在属性的订阅器dep
添加当前watcher实例,从而在属性值有变化的时候,watcher实例就能收到更新通知
Compile
Compile 指令实现,解析指令,模版渲染,更新视图,并将每个指令对应的节点绑定更新函数new Updater(),添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el
转换成文档碎片fragment
进行解析编译操作,解析完成,再将fragment
添加回原来的真实dom节点中;监听数据、绑定更新函数的处理是在compileUtil.bind()
这个方法中,通过new Watcher()
添加回调来接收数据变化的通知
流程图:
完整代码:
1 function Compile(el, vm) { 2 this.$vm = vm; 3 this.$el = this.isElementNode(el) ? el : document.querySelector(el); 4 5 if (this.$el) { 6 this.$fragment = this.node2Fragment(this.$el); 7 this.init(); 8 this.$el.appendChild(this.$fragment); 9 } 10 } 11 12 Compile.prototype = { 13 // dom节点 转化为 Fragment文档碎片 14 node2Fragment: function(el) { 15 var fragment = document.createDocumentFragment(), 16 child; 17 18 // 将原生节点拷贝到fragment 19 while (child = el.firstChild) { 20 fragment.appendChild(child); 21 } 22 23 return fragment; 24 }, 25 26 init: function() { 27 this.compileElement(this.$fragment); 28 }, 29 30 compileElement: function(el) { 31 var childNodes = el.childNodes, 32 me = this; 33 34 [].slice.call(childNodes).forEach(function(node) { 35 var text = node.textContent; // 文本内容 36 var reg = /\{\{(.*)\}\}/; // 匹配{{}}花括号 37 38 if (me.isElementNode(node)) { //节点类型为元素 39 me.compile(node); 40 } else if (me.isTextNode(node) && reg.test(text)) { //节点类型为text 41 me.compileText(node, RegExp.$1); 42 } 43 44 if (node.childNodes && node.childNodes.length) { 45 me.compileElement(node); 46 } 47 }); 48 }, 49 50 // 编译 解析 元素节点 51 compile: function(node) { 52 var nodeAttrs = node.attributes, 53 me = this; 54 55 [].slice.call(nodeAttrs).forEach(function(attr) { 56 var attrName = attr.name; 57 if (me.isDirective(attrName)) { 58 var exp = attr.value; // 属性值 59 var dir = attrName.substring(2); // v-on: 60 if (me.isEventDirective(dir)) { // 事件指令 on 61 compileUtil.eventHandler(node, me.$vm, exp, dir); 62 } else { // 普通指令 63 compileUtil[dir] && compileUtil[dir](node, me.$vm, exp); 64 } 65 66 node.removeAttribute(attrName); 67 } 68 }); 69 }, 70 // 编译 解析 文本节点 71 compileText: function(node, exp) { 72 compileUtil.text(node, this.$vm, exp); 73 }, 74 75 isDirective: function(attr) { 76 return attr.indexOf('v-') == 0; 77 }, 78 79 isEventDirective: function(dir) { 80 return dir.indexOf('on') === 0; 81 }, 82 83 isElementNode: function(node) { 84 return node.nodeType == 1; 85 }, 86 87 isTextNode: function(node) { 88 return node.nodeType == 3; 89 } 90 }; 91 92 // 指令处理集合 93 var compileUtil = { 94 text: function(node, vm, exp) { 95 this.bind(node, vm, exp, 'text'); 96 }, 97 98 html: function(node, vm, exp) { 99 this.bind(node, vm, exp, 'html'); 100 }, 101 102 model: function(node, vm, exp) { 103 this.bind(node, vm, exp, 'model'); 104 105 var me = this, 106 val = this._getVMVal(vm, exp); 107 node.addEventListener('input', function(e) { 108 var newValue = e.target.value; 109 if (val === newValue) { 110 return; 111 } 112 113 me._setVMVal(vm, exp, newValue); 114 val = newValue; 115 }); 116 }, 117 118 class: function(node, vm, exp) { 119 this.bind(node, vm, exp, 'class'); 120 }, 121 122 bind: function(node, vm, exp, dir) { 123 var updaterFn = updater[dir + 'Updater']; 124 // 初始化 渲染视图 125 updaterFn && updaterFn(node, this._getVMVal(vm, exp)); 126 // 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher 127 new Watcher(vm, exp, function(value, oldValue) { 128 // 监测到数据变化,更新视图 129 updaterFn && updaterFn(node, value, oldValue); 130 }); 131 }, 132 133 // 事件处理 134 eventHandler: function(node, vm, exp, dir) { 135 var eventType = dir.split(':')[1], 136 fn = vm.$options.methods && vm.$options.methods[exp]; 137 138 if (eventType && fn) { 139 node.addEventListener(eventType, fn.bind(vm), false); 140 } 141 }, 142 143 _getVMVal: function(vm, exp) { 144 var val = vm; 145 exp = exp.split('.'); 146 exp.forEach(function(k) { 147 val = val[k]; 148 }); 149 return val; 150 }, 151 152 _setVMVal: function(vm, exp, value) { 153 var val = vm; 154 exp = exp.split('.'); 155 exp.forEach(function(k, i) { 156 // 非最后一个key,更新val的值 157 if (i < exp.length - 1) { 158 val = val[k]; 159 } else { 160 val[k] = value; 161 } 162 }); 163 } 164 }; 165 166 // 更新函数集合 167 var updater = { 168 textUpdater: function(node, value) { 169 node.textContent = typeof value == 'undefined' ? '' : value; 170 }, 171 172 htmlUpdater: function(node, value) { 173 node.innerHTML = typeof value == 'undefined' ? '' : value; 174 }, 175 176 classUpdater: function(node, value, oldValue) { 177 var className = node.className; 178 className = className.replace(oldValue, '').replace(/\s$/, ''); 179 180 var space = className && String(value) ? ' ' : ''; 181 182 node.className = className + space + value; 183 }, 184 185 modelUpdater: function(node, value, oldValue) { 186 node.value = typeof value == 'undefined' ? '' : value; 187 } 188 };
MVVM
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,Compile解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
完整代码:
1 function MVVM(options) { 2 this.$options = options || {}; 3 var data = this._data = this.$options.data; 4 var me = this; 5 6 // 数据代理 7 // 实现 vm.xxx -> vm._data.xxx 8 Object.keys(data).forEach(function(key) { 9 me._proxyData(key); 10 }); 11 12 // 初始化computed 13 this._initComputed(); 14 15 // 调用observe,监测数据是否变化 16 observe(data, this); 17 18 // 编译解析指令模板 19 this.$compile = new Compile(options.el || document.body, this) 20 } 21 22 MVVM.prototype = { 23 $watch: function(key, cb, options) { 24 new Watcher(this, key, cb); 25 }, 26 27 _proxyData: function(key, setter, getter) { 28 var me = this; 29 setter = setter || 30 Object.defineProperty(me, key, { 31 configurable: false, 32 enumerable: true, 33 get: function proxyGetter() { 34 return me._data[key]; 35 }, 36 set: function proxySetter(newVal) { 37 me._data[key] = newVal; 38 } 39 }); 40 }, 41 42 _initComputed: function() { 43 var me = this; 44 var computed = this.$options.computed; 45 if (typeof computed === 'object') { 46 Object.keys(computed).forEach(function(key) { 47 Object.defineProperty(me, key, { 48 get: typeof computed[key] === 'function' ? 49 computed[key] : computed[key].get, 50 set: function() {} 51 }); 52 }); 53 } 54 } 55 };
数据双向绑定的简单实现 实例
1 <!DOCTYPE html> 2 <head></head> 3 <body> 4 <div id="app"> 5 <input type="text" id="a" v-model="text"> 6 {{text}} 7 </div> 8 <script type="text/javascript"> 9 function Compile(node, vm) { 10 if(node) { 11 this.$frag = this.nodeToFragment(node, vm); 12 console.log('vm===>', vm); 13 console.log('$frag=>>>',this.$frag) // #document-fragment 14 return this.$frag; 15 } 16 } 17 Compile.prototype = { 18 nodeToFragment: function(node, vm) { 19 var self = this; 20 var frag = document.createDocumentFragment(); 21 var child; 22 23 24 console.log('node=>>>', node) // #app 节点 25 26 27 while(child = node.firstChild) { 28 console.log('child=>>>', child) 29 self.compileElement(child, vm); 30 frag.append(child); // 将所有子节点添加到fragment中 31 } 32 return frag; 33 }, 34 compileElement: function(node, vm) { 35 var reg = /\{\{(.*)\}\}/; 36 console.log('reg===>', reg); 37 console.log('node.nodeType==>>', node.nodeType); 38 39 40 //节点类型为元素 41 if(node.nodeType === 1) { 42 var attr = node.attributes; 43 // 解析属性 44 for(var i = 0; i < attr.length; i++ ) { 45 if(attr[i].nodeName == 'v-model') { 46 var name = attr[i].nodeValue; // 获取v-model绑定的data中的属性名 [text] 47 console.log('name===>',name); 48 node.addEventListener('input', function(e) { 49 // 给相应的data属性赋值,进而触发该属性的set方法 50 vm[name]= e.target.value; 51 }); 52 node.value = vm[name]; // 将data的值赋给该node 53 new Watcher(vm, node, name, 'value'); 54 } 55 }; 56 } 57 //节点类型为text 58 if(node.nodeType === 3) { 59 console.log('node.nodeValue==>>', node.nodeValue); 60 console.log(reg.test(node.nodeValue)) 61 if(reg.test(node.nodeValue)) { 62 var name = RegExp.$1; // 获取匹配到的字符串 63 name = name.trim(); 64 node.nodeValue = vm[name]; // 将data的值赋给该node 65 new Watcher(vm, node, name, 'nodeValue'); 66 console.log(vm, node, name) 67 } 68 } 69 }, 70 } 71 function Dep() { 72 this.subs = []; 73 } 74 Dep.prototype = { 75 addSub: function(sub) { 76 this.subs.push(sub); 77 }, 78 notify: function() { 79 this.subs.forEach(function(sub) { 80 sub.update(); 81 }) 82 } 83 } 84 // node => dom真实节点 85 // name => data 属性key 86 // 87 function Watcher(vm, node, name, type) { 88 Dep.target = this; 89 this.name = name; 90 this.node = node; 91 this.vm = vm; 92 this.type = type; 93 this.update(); 94 Dep.target = null; 95 } 96 97 Watcher.prototype = { 98 update: function() { 99 this.get(); 100 // console.log('node, type=>', this.node, this.type); 101 this.node[this.type] = this.value; // 订阅者执行相应操作 102 }, 103 // 获取data的属性值 104 get: function() { 105 this.value = this.vm[this.name]; //触发相应属性的get 106 // console.log('value, name=>',this.value, this.name) 107 } 108 } 109 function defineReactive (obj, key, val) { 110 var dep = new Dep(); 111 Object.defineProperty(obj, key, { 112 get: function() { 113 console.log(Dep.target) 114 // <input type="text" id="a" v-model="text"> => Watcher {name: "text", node: input#a, vm: Vue, type: "value"} 115 // {{text}} => Watcher {name: "text", node: text, vm: Vue, type: "nodeValue"} 116 //添加订阅者watcher到主题对象Dep 117 if(Dep.target) { 118 // JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用 119 dep.addSub(Dep.target); 120 } 121 return val; 122 }, 123 set: function (newVal) { 124 console.log(newVal === val, val, newVal); 125 if(newVal === val) return; 126 val = newVal; 127 console.log(val); 128 // 作为发布者发出通知 129 dep.notify(); 130 } 131 }) 132 } 133 function observe(obj, vm) { 134 Object.keys(obj).forEach(function(key) { 135 defineReactive(vm, key, obj[key]); 136 }) 137 } 138 139 function Vue(options) { 140 this.data = options.data; 141 var data = this.data; 142 observe(data, this); 143 var id = options.el; 144 // console.log(this) 145 var dom =new Compile(document.getElementById(id),this); 146 147 // 编译完成后,将dom返回到app中 148 document.getElementById(id).appendChild(dom); 149 } 150 var vm = new Vue({ 151 el: 'app', 152 data: { 153 text: 'hello world' 154 } 155 }); 156 console.log(vm) 157 </script> 158 </body> 159 </html>
完整代码:https://github.com/136shine/MVVM_ada
参考:https://www.cnblogs.com/libin-1/p/6893712.html
https://segmentfault.com/a/1190000006599500
http://baijiahao.baidu.com/s?id=1596277899370862119&wfr=spider&for=pc