vue双向绑定原理
前言
vue是一款mvvm前端框架,其vm的双向绑定特性很是让人津津乐道。
Vue.js中的双向数据绑定指的是将视图(View)与模型(Model)之间建立起自动同步关系。
当用户在界面上修改了输入值时,对应的数据会被更新;
反过来,如果后台数据发生变化,也能及时地反映到前端页面上。
我们也都知道其原理是 数据劫持+发布订阅模式,那具体怎么做呢?---下边一一道来。
数据劫持
首先程序需要知道数据被改变了,然后逞能根据改变做出同步的处理。
那怎么知道数据被改变了呢?
vue2和vue3分别利用了不同的手段!
vue2-defineProperty
在vue2时代 是利用 对象属性的 属性描述符(defineProperty)之访问器属性set函数做劫持监听。
defineProperty基本使用方式
const dog = {
name: '小花',
age:20
};
// 利用属性描述符里的set方法,对修改对象属性做监听或劫持
let dogAgeTemp = dog.name;
Object.defineProperty(dog, 'name', {
set(newVal){
dogAgeTemp = newVal;
// 这里可以监听和劫持对象属性的改变
console.log('dog的name有变化,新值为'+newVal);
},
get(){
return dogAgeTemp;
}
})
dog.name = '王五'
console.log(dog);
显然如果要监听一个对象所有的属性,这样一个个手写出来实在太麻烦,我们可以结合遍历
const dog = {
name: '小花',
age: 20
};
// 利用属性描述符里的set方法,对修改对象属性做监听或劫持
for (const key in dog) {
let dogAttrTemp = dog[key];
Object.defineProperty(dog, key, {
set(newVal) {
dogAttrTemp = newVal;
// 这里可以监听和劫持对象属性的改变
console.log('dog的' + key + '有变化,新值为' + newVal);
},
get() {
return dogAttrTemp;
}
})
}
dog.name = '王五'
dog.age = 22
console.log(dog);
显然,这还是不够完美,无法处理对象属性仍然可能是对象的问题,我们可以结合递归
const dog = {
name: '小花',
age: 20,
child: {
cname: '小花花'
}
};
// 利用属性描述符里的set方法,对修改对象属性做监听或劫持
(() => {
const handel = (o) => {
for (const key in o) {
let oAttrTemp = o[key];
const isObject = Object.prototype.toString.call(oAttrTemp)==='[object Object]';
if (isObject) {
console.log('是对象,',oAttrTemp);
handel(oAttrTemp);
} else {
Object.defineProperty(o, key, {
set(newVal) {
oAttrTemp = newVal;
// 这里可以监听和劫持对象属性的改变
console.log('dog的' + key + '有变化,新值为' + newVal);
},
get() {
return oAttrTemp;
}
})
}
}
}
handel(dog);
})();
dog.name = '王五'
dog.age = 22
dog.child.cname = '小王五'
console.log(dog);
至此,算是完成了。
但是我们也能发现两个问题:
后续添加的属性,因为再遍历处理之后加入的,无法被监听。
数组或Set、Map内部的修改无法被监听。
针对一个问题,vue放任没管,而是让开发者注意或者使用$forceUpdate
强制脏值检测,直至es6 proxy的出现。
后者 vue重写了数组的部分常用方法,来达到监听更新的目的。
vue3-proxy
proxy是es6出的一个api,它可以很好地去拦截和代理处理js对象数据。
代理对象和原始对象的改动都会相互影响,不过只有代理对象被改动的时候才会触发内部的set等函数钩子。
const dog = {
name: '小花',
age: 20
};
// 利用代理 里的set对修改对象属性做监听或劫持
const dogProxy = new Proxy(dog, {
set(target, key, val) {
// 可以使用Reflect.set(...arguments) 或 Reflect.set(target, key, val);
target[key] = val;
// 这里可以监听和劫持对象属性的改变
console.log('dog的' + key + '有变化,新值为' + val);
}
});
dogProxy.name = '王五'
dogProxy.age = 20
console.log(dog, dogProxy);
可以看到,代理对象在监听set的时候,多个属性不需要自己手动循环处理,这比es5的属性描述法好用多了。
但Proxy和defineProperty的一个共同特性,不支持对象嵌套,需要递归去实现。
const dog = {
name: '小花',
age: 20,
child: {
cname: '小花花'
}
};
const getProxy = (obj) => {
// 代理处理器
const proxyHandler = {
set(target, key, val) {
// 可以使用Reflect.set(...arguments) 或 Reflect.set(target, key, val);
target[key] = val;
// 这里可以监听和劫持对象属性的改变
console.log( key + '有变化,新值为' + val);
}
}
// 递归处理器
const temp = new Proxy(obj, {});
const reHandler = (o) => {
for (const key in o) {
const isObject = Object.prototype.toString.call(o[key]) === '[object Object]';
if (isObject) {
o[key] = new Proxy(o[key], proxyHandler);
reHandler(o[key])
}
}
}
reHandler(temp);
// 返回结果
return temp
}
const dogProxy = getProxy(dog);
dogProxy.child.cname = '小王五'
console.log(dogProxy);