# vue双向绑定原理 **Repository Path**: autumn_eden/mvvm ## Basic Information - **Project Name**: vue双向绑定原理 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-03-31 - **Last Updated**: 2022-03-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # vue双向绑定原理 ## 1.原理 >vue数据双向绑 定是通过数据劫持结合发布者-订阅者模式的方式,通过 **Object.defineProperty() ** 劫持各个属性的 **setter** , **getter** , 在数据变动时发布消息给订阅者,触发相应的监听回调。 ## 2. 实现思路 > 要实现mvvm 的双向绑定,必须实现以下几点: - 1 实现一个数据监听器 Observer,能够对数据对象的所有属性进行监听,如果变动,可拿到最新值并通知订阅者。 - 2 实现一个指令解析器 Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。 - 3 实现一个 Watcher,作为连接 Observer 和 Compile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。 - 4 实现一个可以容纳订阅者的消息订阅器 Dep ,订阅器 Dep 主要负责收集订阅器,然后在属性变化的时候执行对象的 订阅者的更新函数。 - 5 实现MVVM 入口函数,整合以上三者。 上述流程如图所示: ![image.png](https://upload-images.jianshu.io/upload_images/26716449-ce321162aa57953d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 3. 实现Observer 数据监听器 >Observer 是一个数据监听器,其核心方法就是 Object.defineProperty()。如果要对所有属性都进行监听的话,那么可以通过递归方法遍历所有属性值,并对其进行 Object.defineProperty()处理,如下代码: - Observer.js ``` // 1.实现Observer观察者 function Observer(data) { this.data = data; this.walk(data); } Observer.prototype = { constructor: Observer, walk: function (data) { let self = this; Object.keys(data).forEach(key => { self.defineReacctive(self.data, key, data[key]) }) }, defineReacctive: function (data, key, val) { observe(val);//监听子属性 Object.defineProperty(data, key, { enumerable: true,//可枚举 configurable: false,//不能再define get: function () { return val; }, set: function (newVal) { if (val === newVal) return; console.log('监听到的值发生变化了', val, '-->', newVal); val = newVal; } }) } } function observe(data, vm) { if (!data || typeof data !== 'object') { return; } return new Observer(data) } ``` ## 4. 实现Dep 消息订阅器 > 设计过程中,需要创建一个可以容纳订阅者的消息订阅器 Dep,订阅器 Dep主要负责 收集订阅器Watcher,然后在属性变化的时候执行对象订阅者的更新函数。 - Dep.js ``` // 实现消息订阅器 function Dep() { this.subs = []; } Dep.prototype = { addSub: function (sub) { this.subs.push(sub) }, notify: function () { this.subs.forEach(function (sub) { sub.update(); }) } } Dep.target = null; ``` - 结合Dep 和Observer >将订阅器添加的订阅者 设计在Observer的 getter里面,这是为了让 Watcher 初始化进行触发,在 Observer的setter函数里面,如果数据变化,就会通知所有订阅者,订阅者们就会执行对象的更新函数。一个比较完整的Observer 已经实现了。 ``` Observer.prototype = { constructor: Observer, walk: function (data) { let self = this; Object.keys(data).forEach(key => { self.defineReacctive(self.data, key, data[key]) }) }, defineReacctive: function (data, key, val) { let dep = new Dep(); observe(val);//监听子属性 Object.defineProperty(data, key, { enumerable: true,//可枚举 configurable: false,//不能再define get: function () { //将订阅者Wather赋予给 Dep.target,每个订阅者都是不一样的 Dep.target && dep.addSub(Dep.target);//在这里添加一个订阅器 return val; }, set: function (newVal) { if (val === newVal) return; console.log('监听到的值发生变化了', val, '-->', newVal); val = newVal; dep.notify();//通知所有订阅者 } }) } } ``` ## 5. 实现Compile 编译指令 > Compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面,并将每个指定对应的节点绑定更新函数,添加监听数据的Watcher订阅者,一旦数据有变动,收到通知,更新视图,如图所示: ![image.png](https://upload-images.jianshu.io/upload_images/26716449-b0e02f2280c95f90.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - Compile.js 代码如下: ``` // 编译指令 function Compile(el, vm) { this.$vm = vm; this.$el = this.isElementNode(el) ? el : document.querySelector(el); if (this.$el) { this.$fragment = this.nodeFragment(this.$el); this.init(); this.$el.appendChild(this.$fragment); } } Compile.prototype = { constructor: Compile, nodeFragment: function (el) { let fragment = document.createDocumentFragment(); let child; //将原生节点拷贝到fragment while (child = el.firstChild) { fragment.appendChild(child); } return fragment; }, init: function () { this.compileElement(this.$fragment); }, compileElement: function (el) { let childNodes = el.childNodes; let self = this; [].slice.call(childNodes).forEach(function (node) { let text = node.textContent; let reg = /\{\{(.*)\}\}/; //匹配 {{}} if (self.isElementNode(node)) {//元素节点 self.compile(node) } else if (self.isTextNode(node) && reg.test(text)) { //文本节点且 {{}} self.compileText(node, RegExp.$1.trim()); } if (node.childNodes && node.childNodes.length) {//拥有孩子节点,继续递归 self.compileElement(node) } }) }, compile: function (node) { let nodeAttrs = node.attributes; let self = this; [].slice.call(nodeAttrs).forEach(function (attr) { let attrName = attr.name; if (self.isDirective(attrName)) { let exp = attr.value; let dir = attrName.substring(2); if (self.isEventDirective(dir)) {//事件指令 compileUtil.eventHandler(node, self.$vm, exp, dir); } else {//普通指令 compileUtil[dir] && compileUtil[dir](node, self.$vm, exp) } node.removeAttribute(attrName); } }) }, compileText: function (node, exp) { compileUtil.text(node, this.$vm, exp); }, isDirective: function (attr) {//普通指令 return attr.indexOf('v-') == 0; }, isEventDirective: function (dir) {//事件指令 return dir.indexOf('on') === 0; }, isElementNode: function (node) {//元素节点 return node.nodeType == 1; }, isTextNode: function (node) {//文本节点 return node.nodeType == 3; }, } //指定处理集合 let compileUtil = { text: function (node, vm, exp) { this.bind(node, vm, exp, 'text'); }, html: function (node, vm, exp) { this.bind(node, vm, exp, 'html'); }, model: function (node, vm, exp) { this.bind(node, vm, exp, 'model'); let self = this; let val = this._getVMVal(vm, exp); node.addEventListener('input', function (e) { let newVal = e.target.value; if (val === newVal) return; console.log(newVal); self._setVMVal(vm, exp, newVal); val = newVal }) }, class: function (node, vm, exp) { this.bind(node, vm, exp, 'class'); }, bind: function (node, vm, exp, dir) { let updateFn = updater[dir + 'Updater']; updateFn && updateFn(node, this._getVMVal(vm, exp)); new Watcher(vm, exp, function (value, oldValue) { updateFn && updateFn(node, value, oldValue) }) }, //事件处理 eventHandler: function (node, vm, exp, dir) { let eventType = dir.split(':')[1]; let fn = vm.$options.methods && vm.$options.methods[exp]; if (eventType && fn) { node.addEventListener(eventType, fn.bind(vm), false); } }, _getVMVal:function(vm,exp) { let val = vm; exp = exp.split('.'); exp.forEach(function(k) { val = val._data[k] }) return val; }, _setVMVal: function(vm,exp,value) { let val = vm; exp = exp.split('.'); exp.forEach(function(k,i) { //非最后一个key,更新val的值 if(i Watcher 订阅者作为 Observer 和 Compile 之间通信的桥梁,主要做的事情是: 1.在自身实例化时往属性订阅器Dep 里面添加自己:在Dep.target上缓存订阅器,通过触发 getter方法,把自己添加到getter方法里面,添加成功后去掉Dep.target。 2.自身必须有一个update方法。 3.待属性变动dep.notice()通知时,能调用自身的update方法,并触发Compil中绑定的回调。 ``` function MVVM(options) { this.$options = options || {}; let data = this._data = this.$options.data; let self = this; observe(data,self); this.$compile = new Compile(options.el || document.body, self) } ``` 从上面代码可看出监听的数据对象是options.data,每次需要更新视图,则必须通过**let vm = new MVVM({data:{name: 'zs'}}); vm._data.name = 'li';**,这样的方式来改变数据。 显然不符合我们一开始的期望,我们所期望的调用方式应该是这样的: **let vm = new MVVM({data:{name: 'zs'}}); vm.name = 'li';** 所以这里我们需要给MVVM实例添加一个属性代理的方法,使访问vm的属性代理可访问 vm._data的属性,改造后的代码如下: ``` function MVVM(options) { this.$options = options || {}; console.log(this); let data = this._data = this.$options.data; let self = this; //数据代理 //实现 vm.xxx -> vm._data.xxx Object.keys(data).forEach(function (key) { self._proxyData(key) }); this._initComputed(); observe(data); this.$compile = new Compile(options.el || document.body, this) } MVVM.prototype = { constructor: MVVM, $watch: function (key, options, cb) { new Watcher(this, key, cb); }, _proxyData: function (key, setter, getter) { let self = this; setter = setter || Object.defineProperty(self, key, { enumerable: true, configurable: false, get: function proxyGetter() { return self._data[key] }, set: function proxySetter(newVal) { self._data[key] = newVal; } }) }, _initComputed: function () {//添加计算属性 let self = this; let computed = this.$options.computed; if (typeof computed === 'object') { Object.keys(computed).forEach(function (key) { Object.defineProperty(self, key, { get: typeof computed[key] === 'function' ? computed[key] : computed[key].get, set: function() {} }) }) } }, } ``` 这里主要利用了 **Object.defineProperty()** 这个方法来劫持 vm实例对象的属性的读写权,使读写vm实例的属性 转成了 vm._data的属性值。 ## 7. html ```

{{name}}

{{msg}}

``` 效果图: ![image.png](https://upload-images.jianshu.io/upload_images/26716449-e43f38dc6dc79359.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)