Vue有一个很方便的特性就是Vue的双向绑定,即响应式变化,在Vue2.X版本中,Vue响应式变化靠的是Object.defineProperty方法实现的,但是这个方法有个问题,就是对数组的支持不全面,如我们想要通过arr[0] = 11这种下标修改值的方式,Vue是不会监听并重新渲染组件的,以及arr.length = 0这种方式清空数组,也是不支持的。
那么在Vue中,是如何实现数据的双向绑定的呢?我们可以简单模拟一下。
实现原理就是:给数据绑定get/set方法,当修改对应的属性值时,会触发对应的get/set方法,在get/set方法中实现绑定逻辑。如果是数组,会重写数组的方法,如果是对象,会使用Object.defineProperty方法。
先定义一个最简单的对象obj
let obj = {
name:"Alice",
age:18
}
定义一个渲染函数
function render(){
console.log("渲染")
}
给对象obj的每一个属性都绑定get/set方法
let handler = function(obj,key,value){
Object.defineProperty(obj,key,{
get(){
return value;
},
set(newValue){
if(value!==newValue){
value = newValue;
// 修改属性值时,调用渲染函数
render();
}
}
});
}
调用handler方法给obj的属性绑定get/set
function observe(obj){
for(let k in obj){
handler(obj,k,obj[k])
}
}
observer(obj);
obj.name = "Fiona" // 此时修改obj的属性值,会触发name的set方法,然后调用render函数,重新渲染
上面介绍了最简单的对象的数据双向绑定逻辑,那如果我们的对象中的属性值又是一个对象呢?如下
obj = {
name:{"name1":"Alice"}
}
这时候,我们给name绑定了get/set方法,但是name1并没有绑定,所以我们在使用obj.name.name1 = "Fiona"的时候,是没有办法触发name1的set方法的。
我们再对上面的方法进行改进:
var handler = function(obj,key,value){
// value可能还是对象,所以需要再次给它绑定get/set方法
observe(value);
Object.defineProperty(obj,key,{
get(){
return value;
},
set(newValue){ // 设置的值,可能还是一个对象,所以需要将设置的值的对象属性也绑定get/set方法
observe(value);
if(value !== newValue){
value = newValue;
render();
}
}
})
}
function observe(obj){
if(typeof(obj)!=="object" || obj === null){
return obj
}
for (let k in obj){
handler(obj,k,obj[k]);
}
}
observe(obj);
obj.name.name1 = 'Fiona'; //此时会触发name1的set方法,调用render()obj.age = {"age1":19}obj.age.age1 = 20
到这里,对象【非数组】的响应式变化就完成了,但是这里还有一个问题,就是如果想要给对象新增一个属性,也是不会触发的
如上面的obj,想给它新增一个gender,是不会触发set方法的,想要给对象新增属性同时触发set方法,需要使用$set方法
vm.$set(obj,newProperty,value)
接下来我们再看看数组的响应式变化的实现
数组需要重写数组的所有方法。但是还是不支持数组的长度变化,也不支持通过数组的下标修改值的方法
数组常用的方法有7个:pop, push, shift, unshift, splice, sort, reverse
// 数组的常用方法
let methods = ['pop','push', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
// 数组原型链上的方法
let ArrayProto = Array.prototype
// 我们需要重写数组的所有方法,但是又不能影响原有的原型链,所以这里需要将原型链重新拷贝一份
let proto = Object.create(ArrayProto);
// 循环数组的所有方法,给拷贝的原型链复制
methods.forEach(fn=>{
proto[fn] = function(){
// 调用原有原型链的方法
ArrayProto[fn].call(this, ...arguments);
}
}
// 定义渲染函数
function render(){
console.log("渲染...")
}
var handler = function(obj, key, value){
observe(obj);
Object.defineProperty = function(obj, key, {
get(){
return value;
},
set(newValue){
observe(newValue);
if(value != newValue){
value = newValue;
render();
}
}
})
}
function observe(obj){
if(Array.isArray(obj)){
// 如果obj是数组,将我们自定义的原型链复制给obj的原型链
obj.__proto__ = proto;
return;
}
if(typeof(obj) !=="object" || obj===null){
return obj;
}
for(let k in obj){
handler(obj, k, obj[k]);
}
}