一篇文章带你彻底了解Vue.js如何实现数据双向绑定

203 阅读4分钟

Object.defineProperty中的秘密

学习过Vue.js的小伙伴都知道,Vue.js的核心在于组件化开发和数据的双向绑定来实现响应式布局,而在Vue2.x中提到数据的双向绑定,就一定会想到Object.defineProperty(),下面先来介绍一下Vue.js是如何实现数据的双向绑定的吧!

一、数据双向绑定的原理

一张图片描述出Vue.js实现数据双向绑定的过程

  1. 首先实现了一个监听器observer:对数据对象进行遍历,包括子属性对象的属性,利用Object.defineProperty()方法给属性都加上get()和set()方法,这样给这个对象的某个值进行赋值的时候就会触发set()方法,这样就会监听到了数据的变化。

    let data = {name:'xiaomao'}; observer(data); data.name = 'wangxuejiao'; //值发生了变化

    function observer(data){ if(!data && typeof data !== 'object'){ return; } //取出所有属性进行遍历 Object.keys(data).forEach(function(key){ defineReactive(data, key, data[key]); }) }

    function defineReactive(data,key,val){ observer(val); //监听子属性 Object.defineProperty(data, key, { enumerable: true, // 可枚举 configurable: false, // 不能再define get: function() { return val; }, set: function(newVal) { console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal); val = newVal; } }); }

  2. 实现了一个订阅器Dep:本质上是一个数组,采用发布——订阅的设计模式,用来收集订阅者watcher,实现对监听器observer和订阅者watcher的统一管理

    function Dep() { this.subs = []; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach(function(sub) { sub.update(); }); } };

  3. 实现了一个订阅者watcher:可以收到属性变化的通知并执行相应的函数,从而更新视图,是observer和complie之间的桥梁,主要功能是订阅observer中属性值变化的消息,当收到消息时,触发解析器compile中对应的更新函数。

    function watcher(vm,exp,cb){ this.vm = vm; this.exp = exp; this.vm = vm; //此处为了触发属性的getter,从而在dep里面添加自己,结合Observer更易理解 this.value = this.get(); } Watcher.prototype = { update:function(){ this.run(); //属性值变化收到通知 }, run:function(){ var value = this.get(); //取到最新值 var oldVal = this.value; if(value !== oldValue){ this.value = value; this.cb.call(this.vm,value,oldVal); //执行Compile中绑定的回调,更新视图 } } get:function(){ Dep.target = this; //将当前订阅者指向自己 var value = this.vm[exp]; //触发getter,添加自己到属性订阅器中 Dep.target = null; //添加完毕,重置 return value; } }

  4. 实现了一个解析器compile:解析Vue模板指令,将模板中变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点,绑定更新数据,添加监听数据的订阅者,一旦数据有变动收到通知调用更新函数进行数据更新。

    function Compile(el) { this.el=this.isElementNode(el)?el:document.querySelector(el);if(this.el = this.isElementNode(el) ? el : document.querySelector(el); if (this.el) { this.fragment=this.node2Fragment(this.fragment = this.node2Fragment(this.el); this.init(); this.el.appendChild(this.el.appendChild(this.fragment); } } Compile.prototype = { init: function() { this.compileElement(this.fragment); }, node2Fragment: function(el) { var fragment = document.createDocumentFragment(), child; // 将原生节点拷贝到fragment while (child = el.firstChild) { fragment.appendChild(child); } return fragment; }, compileElement: function(el) { var childNodes = el.childNodes, me = this; [].slice.call(childNodes).forEach(function(node) { var text = node.textContent; var reg = /\{\{(.*)\}\}/; // 表达式文本 // 按元素节点方式编译 if (me.isElementNode(node)) { me.compile(node); } else if (me.isTextNode(node) && reg.test(text)) { me.compileText(node, RegExp.1); } // 遍历编译子节点 if (node.childNodes && node.childNodes.length) { me.compileElement(node); } }); }, compile: function(node) { var nodeAttrs = node.attributes, me = this; [].slice.call(nodeAttrs).forEach(function(attr) { // 规定:指令以 v-xxx 命名 // 如 中指令为 v-text var attrName = attr.name; // v-text if (me.isDirective(attrName)) { var exp = attr.value; // content var dir = attrName.substring(2); // text if (me.isEventDirective(dir)) { // 事件指令, 如 v-on:click compileUtil.eventHandler(node, me.vm, exp, dir); } else { // 普通指令 compileUtil[dir] && compileUtil[dir](node, me.vm, exp); } } }); }, // 指令处理集合 var compileUtil = { text: function(node, vm, exp) { this.bind(node, vm, exp, 'text'); }, // ...省略 bind: function(node, vm, exp, dir) { var updaterFn = updater[dir + 'Updater']; // 第一次初始化视图 updaterFn && updaterFn(node, vm[exp]); // 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher new Watcher(vm, exp, function(value, oldValue) { // 一旦属性值有变化,会收到通知执行此更新函数,更新视图 updaterFn && updaterFn(node, value, oldValue); }); } }

总结:

  • 整体主要是给data对象每一个属性,通过Object.defineProperty()添加get()以及set()方法
  • 在get()方法中通过闭包使用消息订阅器数组添加存放订阅器对象,然后返回属性值
  • 在set()方法中通过先判断新旧值是否一致,如果不一致,则把新值赋给属性,并且调用订阅器的notify方法,这个方法会去通知所有订阅者,订阅者就会去执行对应的更新函数

二、Object.defineProperty()存在的缺陷

Object.defineProperty()是javascript中监听数据变化的方法,vue2.x采用Object.defineProperty()来实现数据双向绑定,但是它却存在一些缺陷,所以在Vue3.0中,尤大大放弃使用Object.defineProperty()而是采用Proxy来实现数据双向绑定,下面就来总结一下Object.defineProperty()到底有哪些缺点才会让尤大大放弃使用呢

  1. 对于对象而言,它无法检测到对象属性的新增或删除

由于Vue会在初始化实例的时候对属性执行getter和setter转化,所以属性必须在data对象上存在才能将它转化成响应式的

对于已经创建的实例,Vue不允许动态添加根级别的响应式属性,但是可以使用Vue.set(object,propertyName,value)方法向嵌套对象添加响应式属性

Vue.set(obj,'name','wxj');
Vue.$set(thgis.obj,'name','wxj');

如果想要添加多个属性,使用Object.assign(),这样添加到对象身上的属性也不会触发更新,应该用原对象与要混合进去的对象的属性一起创建一个新的对象

this.obj = Object.assign({},this.obj, { b: 1, e: 2 })
this.$set(this.obj,'f',0)
this.obj = {...this.obj,...{ b: 3, e: 2 }}

删除属性

Vue.delete(obj, propertyName/index);
vue.$delete(obj, propertyName/index);
  1. 对于数组而言,无法监听以下数组的变化
  • 利用一个索引值直接设置一个数组项

    Vue.set(vm.items,name,newValue);

  • 修改数组的长度

    Vue.items.splice(newLength);

  • 以下七种方法不能实现响应式,Vue重写了这七种方法:push pop shift unshift splice sort reverse