双向绑定
所谓的双向绑定其实就是,ui或者数据有一方做了修改,那么另外一个也会随着改变。简单来说,视图驱动数据,同时数据也能驱动视图。
视图驱动数据,只需要绑定事件。
数据驱动视图,则需要去对数据做监听,我们通常称之为”数据劫持“(在每一次数据改变的时候,去执行更新视图的操作。)
Vue2 - Object.defineProperty
在vue2.x版本中,数据劫持是用过Object.defineProperty这个API来实现
实现原理:
var message = 'abc';
const data = {};
Object.defineProperty(data, 'message', {
get() {
return message;
},
set(newVal) {
message = newVal + '123';
}
});
data.message // 'abc'
data.message = 'test' // 'test123'
读取对象属性,走get方法。修改对象属性。走set方法。
vue中如何对所有属性进行劫持?
只需遍历所有data对象中的所有属性,并对每一个属性使用Object.defineProperty劫持即可,当属性的值发生变化的时候,我们执行一系列的渲染视图的操作。
// 这是数据
const data = {
text:'abc',
number:123,
info: {
sex: '男',
nameInfo: {name:'三叠云'}
}
}
// 函数提升
observer(data);
// 遍历具体对象
function observer(target) {
if (typeof target !== 'object' || !target) {
return target;
}
for (const key in target) {
if (target.hasOwnProperty(key)) {
const value = target[key];
observerObject(target, key, value);
}
}
}
// 递归劫持对象里的属性
function observerObject(target, name, value) {
if (typeof value === 'object' || Array.isArray(target)) {
observer(value);
}
Object.defineProperty(target, name, {
get() {
return value;
},
set(newVal) {
if (newVal !== value) {
if (typeof value === 'object' || Array.isArray(value)) {
observer(value);
}
value = newVal;
}
// 触发视图渲染
renderView();
}
});
}
遍历这个data对象,对每一个属性都使用observerObject方法进行数据劫持。
observerObject主要做的就是使用Object.defineProperty去监听传入的属性,如果target是一个对象的话,就递归执行observer,确保data中所有的对象中的所以属性都能够被监听到。当我们set的时候,去执行renderView(执行视图渲染相关逻辑)。
但。这只能作用于对象上。如果是数组,我们需要换种思维劫持数据:
在数组中,我们知道能够改变数组本身的方法只有七种:
push
pop
shift
unshift
splice
sort
reverse
而我们只需要修改数组的原型方法,往这些方法里添加一些视图渲染的操作。
// 创建一个继承数组原型链的对象
const oldArrayProperty = Array.prototype; // 数组对象原型链上的属性对象
const newArrayProperty = Object.create(oldArrayProperty); // 创建继承了oldArrayProperty对象的对象
Object.create的用法可以参见【附录:Object.create】
['pop', 'push', 'shift', 'unshift', 'splice'].forEach((method) => {
newArrayProperty[method] = function() {
renderView();
oldArrayProperty[method].call(this, ...arguments);
};
});
// 在observer函数中加入数组的判断,如果传入的是数组,则改变数组的原型对象为我们修改过后的原型。
if (Array.isArray(data)) {
data.__proto__ = newArrayProperty;
}
上面代码就是vue2x版本数据劫持的原理实现。
现在我们可以看出Object.defineProperty的一些问题:
递归遍历所有的对象的属性,这样如果我们数据层级比较深的话,是一件很耗费性能的事情
只能应用在对象上,不能用于数组
只能够监听定义时的属性,不能监听新加的属性,这也就是为什么在vue中要使用Vue.set的原因,删除也是同理
$set的原理可以见附录:Vue.set
Vue3 - Proxy
在Vue3中则是使用Proxy来进行数据劫持,Proxy不同于Object.defineProperty的是,它是对整个数据对象进行数据劫持,而Object.defineProperty是对数据对象的某个属性进行数据劫持(如果是多层需要循环绑定)。
Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。IE不兼容。
// 语法
const p = new Proxy(target, handler)
参数:
target: 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler: 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
handler 对象包含有 Proxy 的各个捕获器:
handler.defineProperty()
handler.has()//in 操作符的捕捉器。
handler.get(target, property)
handler.set(target, property, value)
handler.deleteProperty()//delete 操作符的捕捉器
......
Proxy如何工作?
① 监控数组下标变化:
let arr=[1,2,3]
let handler={
get(target, key, receiver) {
console.log('get的key为 ===>' + key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver){
console.log('set的key为 ===>' + key, value);
// 插入
return Reflect.set(target, key, value, receiver);
}
}
let p=new Proxy(arr,handler);
p[1]
< get的key为 ===>1
< 2
p[2] = 5
< set的key为 ===>2 3
< 3
reflect也是es6的语法,再proxy使用中常用到reflect。这俩其实没关系,只是搭配着使用,proxy用来拦截,reflect用来操作。
详可见=》附录:reflect对象
① Proxy的receiver是什么呢?
② Vue3的响应式,为什么 Proxy 要配合 Reflect 一起使用,不能直接使用target[key]返回?
先看代码:
const data = [0,1,2];
let proxyData = new Proxy(data,{
get(target,key,receiver) {
console.log(proxyData === receiver);
return target[key]
}
})
proxyData[0]; // 0
< true
这里返回了true。那么 receiver 是可以表示代理对象的实例。
let parent = {
name: 'abc'
}
let proxy = new Proxy(parent, {
get(target, key, receiver) {
console.log(receiver === proxy);
console.log(receiver === child);
return target[key];
}
})
let child = { text: "123" };
// 设置 child 继承 代理对象 proxy
Object.setPrototypeOf(child, proxy);
child.name // false true
proxy.name // true false
我们可以清楚的看到 receiver 代表的是 继承了 代理对象 proxy 的 child。
到这里,我们明白了 Proxy 中 get 方法的 receiver 参数不仅仅代表 Proxy 的实例,某种情况下,他也会代表继承 Proxy 的那个对象。
let parent = {
name: "Tom",
get value() {
return this.name;
},
};
let proxy = new Proxy(parent, {
get(target, key, receiver) {
return Reflect.get(target, key);
},
});
let child = { name: "小Tom" };
// 设置 child 继承 代理对象 proxy
Object.setPrototypeOf(child, proxy);
console.log(child.value); //Tom
打印 child.value 控制台输出的是 "Tom"。
我们分析下上面代码:
当我们调用 child.value 的时候,child 本身并不存在 value 属性。
但是它继承的 proxy 对象中存在 value 属性的访问方法。
所以触发 proxy 上的 get value(),同时由于访问了 proxy 上的 value属性,所以触发 proxy 的 get 方法。
get 方法的 target 参数就是 parent,key 参数 就是 value。
然后方法中 return Reflect.get(target, key) 相当于 target[key]。
此时,我们访问的 child 对象的 value 属性,return 的却是 parent 的 value 属性。this.name的指向
不知不觉中 this 指向在 get 方法中被偷偷修改了,原本调用的 child 在 get 方法中 变成了 parent。
所以打印出来的是 parent[value],也就是 "Tom"。
这显然不是我们期望的结果,当我们访问 child.value 时,我们希望输出 child 对象的 name 属性,也就是 "小Tom"。
那么,Reflect 中 get 方法的 receiver 参数该上场了。
let parent = {
name: "Tom",
get value() {
return this.name;
},
};
let proxy = new Proxy(parent, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
});
let child = { name: "小Tom" };
// 设置 child 继承 代理对象 proxy
Object.setPrototypeOf(child, proxy);
console.log(child.value); // 小Tom
上面的代码和前面的一样,只是把 get 方法中 returnReflect.get(target, key) 修改成了
return Reflect.get(target, key, receiver) 之后,打印出来的就是我们期望的结果了。
原理很简单:
首先,我们之前提到过 Proxy 中 get 方法的 receiver 参数不仅仅表示代理对象本身,同时也有可能是继承了代理对象的对象,具体区别于调用方。
这里显然他是指向继承了 proxy 的 child。
然后,我们在 Reflect 中的 get 方法第三个参数传入了 Proxy 中的 receiver,也就相当于 child 作为形参,它会修改调用时的 this 指向。
Reflect 中的 receiver 参数可以把属性访问中的 this 指向 receiver 对象。
Vue3中的 Proxy + Reflect 解决了 Vue2 Object.defineProperty中遇到的哪些问题?
一次只能对一个属性进行监听,需要遍历来对所有属性监听。这个我们在上面已经解决了。
在遇到一个对象的属性还是一个对象的情况下,需要递归监听。
对于对象的新增属性,需要手动监听
对于数组通过push、unshift等方法增加的元素,也无法监听
附录:Object.create
Object.create : 创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)
//创建一个Obj对象
var Obj ={
name:'mini',
age:3,
show:function () {
console.log(this.name +" is " +this.age);
}
}
//MyObj 继承obj, prototype指向Obj
var MyObj = Object.create(Obj,{
like:{
value:"fish", // 初始化赋值
writable:true, // 是否是可改写的
configurable:true, // 是否能够删除,是否能够被修改
enumerable:true //是否可以用for in 进行枚举
},
hate:{
configurable:true,
// 有get就不能有value
get:function () { console.log(111); return "mouse" },
// get对象hate属性时触发的方法
set:function (value) {
// set对象hate属性时触发的方法
console.log(value,2222);
return value;
}
}
});
附录:Vue.set
为什么要用Vue.set
主流JavaScript版本的限制 (以及废弃 Object.observe)。Vue2.x 不能检测数组和对象的变化。
#关于对象
Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的
<template>
...
{{data}}
<a-button @click="myfn">执行</a-button>
...
</template>
export default class text extends Vue {
...
data = {a:4}
myfn() {
this.data.b = 5
}
...
}
执行结果为:{a:4} 可以看到,在非初始化状态下往this.data上添加b属性,是非响应式的。
myfn() {
this.data.a = 5
}
// 这时是响应式的
data = {
a: { d: 5 },
};
myfn() {
this.data.a = { d: 5, c: 4 };
};
// 这时是响应式的
data = {
a: { d: 5 },
};
myfn() {
this.data.a.c = 4
};
// 这时是非响应式的
对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。对于不存在的属性。Vue无法对其进行数据劫持。
但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。
data = {
a: { d: 5 },
};
myfn() {
this.$set(this.data.a, 'c', 4);
}
// 此时是响应式的
数组用法:
this.$set(this.dataArray, index, newValue)
Vue.set工作原理
附录:reflect对象
Reflect 是一个window 内置的一个全局对象,它提供拦截 JavaScript 操作的方法
注意:Reflect 不支持 ie浏览器,所以Vue3不支持ie
与大多数全局对象不同,Reflect并非一个构造函数,所以不能通过new 运算符对其进行调用,或者将Reflect对象作为一个函数来调用。Reflect的所有属性和方法都是静态的(就像Math对象)
reflect语法与proxy一样。常见用法:
一:
Reflect.get(target, propertyKey[, receiver])
target: 需要取值的目标对象
propertyKey: 需要获取的值的键值
receiver:receiver则为getter调用时的this值。
const data = ['a','b','c']
Reflect.get(data,2) // c
const data2 = {name:'dj',age:18}
Reflect.get(data2,'name') //dj
二:
Reflect.set(target, propertyKey, value[, receiver])
为对象设置或修改属性值
const data = [1,2,3]
Reflect.set(data,0,99)
console.log(data) // [99,2,3]
Reflect.set(data,'length', 1);
console.log(data) // [99]
Reflect.set(data,'length', 3);
console.log(data) // [99, 空, 空]
var obj = {};
Reflect.set(obj, "a", 123); // true
obj.a; // 123
三:
Reflect.deleteProperty(target, propertyKey)
用于删除属性
var obj = { x: 1, y: 2 };
Reflect.deleteProperty(obj, "x"); // true
obj; // { y: 2 }
var arr = [1, 2, 3, 4, 5];
Reflect.deleteProperty(arr, "3"); // true
arr; // [1, 2, 3, , 5]
arr[3] // undefined
// 如果属性不存在,返回 true
Reflect.deleteProperty({}, "foo"); // true
// 如果属性不可配置,返回 false
Reflect.deleteProperty(Object.freeze({foo: 1}), "foo"); // false
....还有很多语法应用