Object.defineProperty中的秘密
学习过Vue.js的小伙伴都知道,Vue.js的核心在于组件化开发和数据的双向绑定来实现响应式布局,而在Vue2.x中提到数据的双向绑定,就一定会想到Object.defineProperty(),下面先来介绍一下Vue.js是如何实现数据的双向绑定的吧!
一、数据双向绑定的原理
- 首先实现了一个监听器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;
}
});
}
- 实现了一个订阅器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();
});
}
};
- 实现了一个订阅者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;
}
}
- 实现了一个解析器compile:解析Vue模板指令,将模板中变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点,绑定更新数据,添加监听数据的订阅者,一旦数据有变动收到通知调用更新函数进行数据更新。
function Compile(el) {
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el);
this.init();
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 命名
// 如 <span v-text="content"></span> 中指令为 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()到底有哪些缺点才会让尤大大放弃使用呢
- 对于对象而言,它无法检测到对象属性的新增或删除
由于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);
- 对于数组而言,无法监听以下数组的变化
- 利用一个索引值直接设置一个数组项
Vue.set(vm.items,name,newValue);
- 修改数组的长度
Vue.items.splice(newLength);
- 以下七种方法不能实现响应式,Vue重写了这七种方法:push pop shift unshift splice sort reverse