数据代理
如果使用Object.defineProperty()定义属性,打控制台看见的属性就不会直接显示出来,而是以图片上面的这种形式。因此,从这里看出age和name是使用了代理。
attention!!!
当我们在模板里面使用Vue对象的data对象里面的属性时,Vue经过了一系列的步骤。为什么我们能直接使用data里面的属性,而不需要使用data.prop呢?
原理是:Vue实例上的data在Vue中是无法直接访问的,而是将data对象赋值给了_data,因此我们可以直接在Vue实例上操作_data。然后,使用数据代理Object.defineProperty()把_data对象里面的所有属性添加到Vue实例上,并为他们增加get和set方法。
这里有一个坑:在平时我们对象上定义属性上,在get和set方法中如果是又使用了该属性,就会造成死循环,导致栈溢出。
Object.defineProperty(obj, "name", {
get () {
return obj.name
}
})
obj.name;
RangeError: Maximum call stack size exceede
// at Object.get [as name] (F:\code\test\1.js:2662:9)
// at Object.get [as name] (F:\code\test\1.js:2662:20)
// at Object.get [as name] (F:\code\test\1.js:2662:20)
// at Object.get [as name] (F:\code\test\1.js:2662:20)
// at Object.get [as name] (F:\code\test\1.js:2662:20)
// at Object.get [as name] (F:\code\test\1.js:2662:20)
// at Object.get [as name] (F:\code\test\1.js:2662:20)
// at Object.get [as name] (F:\code\test\1.js:2662:20)
// at Object.get [as name] (F:\code\test\1.js:2662:20)
// at Object.get [as name] (F:\code\test\1.js:2662:20)
因此,我们可以使用构造函数,将属性定义在实例上面。
function Observe(obj) {
Object.keys(obj).forEach((key) => {
Object.defineProperty(this, key, {
get() {
return obj[key];
},
set(newVal) {
obj[key] = newVal;
}
})
})
}
这样我们就可以直接在Vue实例上面使用data里面的属性啦~~这样明显可以看出使用数据代理的好处吧,就是在使用data里面的属性时,不需要写很长的一串对象调用(Vue._data.name),而是直接使用Vue实例上定义的属性即可(this.name)。
经过了数据代理以后,只要我们修改Vue实例上的属性时,都会将这个改动映射到_data中。
在这里,我们总结以下数据代理:
1、Vue中的数据代理: 通过Vue实例对象来代理data中属性的操作(读/写)
2、Vue中数据代理的好处: 更加方便的操作data中的数据
3、基本原理: 通过Object.defineProperty()把data上的所有属性添加到Vue实例上 为每一个添加到Vue实例的属性,都指定一个get和set方法 在get和set内部去操作data中对象的属性
数据劫持
其实,在进行数据代理之前,还有一个关键的步骤,就是对_data进行加工。一开始将data赋值给_data的时候就有点疑惑,为什么要有这一步呢?这样做会不会显得有点多余,为何不直接把data拿去做数据代理呢?下面就来探究一下。
试想一下,当我们进行数据代理了以后,一旦我们修改Vue实例上的属性,经过代理,_data里面的属性值也会相应改变。我们希望的效果是:当修改了Vue实例上的属性,页面元素也会发生变化,这里就是答案了。在这里进行了一步数据劫持。当_data里面的数据发生变化时,dom元素也会相应改变,这就需要我们监视_data中属性值的变化啦,我们需要为_data中的所有属性添加get和set方法,一旦发现有变化,就会立即重新编译模板,更新dom元素。
在这里我们需要注意,文章里面说的将data赋值给_data,这里就不是简单的赋值而已,而是通过数据劫持,把data中所有的属性定义在_data中,并为所有的属性添加get和set方法。这样,_data中的数据发生改变时,data里面的数据也会同时改变,我们知道,一旦data里面的属性值发生改变,就会当前的模板重新编译,更新dom元素。这里是一个单向流的操作。
还有需要注意的一点是:一般的我们的data对象都不简单的只有单层结构的属性,里面的数据结构可能会很复杂。先考虑对象的话,data对象里面的属性值可能又是个对象,因此我们在Observe构造函数中,要使用递归,给每个属性值都添加get和set方法。
下面定义一个data对象,
data () {
return {
num: 1,
student: {
name: "aa",
age: 19,
address: "成都"
}
}
}
监听对象
接下来有一个需求,在代码里面我们想向student对象里面添加一个sex属性,并且这个数据要是响应式的。
从上面的截图,可以看出,这个sex属性确实被添加到Vue实例上了,但是并没有为它定义get和set方法,所有肯定不会是响应式的。
读取一个对象中不存在的值返回undefined,不会报错。在Vue中,如果值是undefined,该值是不会显示到页面中的。
这里就要用到Vue的API里面的set方法了。
使用Vue.set以后,就可以发现sex属性添加了get和set方法
在这里,Vue.set ()= this.$set()
但是,set有一个缺陷,就是它的一个参数不能是Vue实例或者Vue实例的根数据对象。
监听数组
接下来, 我们在student对象中添加一个数组,
data () {
return {
num: 1,
student: {
name: "aa",
age: 19,
address: "成都",
hobby: ["aa", "bb", "cc"]
}
}
}
明显可以看出,Vue并没有对数组进行数据代理,对数组的某一个元素进行修改,不会实现响应式。
那数组是怎么实现响应式的呢?
Vue将数组被监听的变更方法进行了包裹,只要操作数组时,调用的是这些方法,就会触发视图更新。这些变更方法是:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
变更方法,顾名思义就是会改变调用了这些方法的原始数组,即会改变原数组的方法。
如果调用非变更方法,可以把改变了的数组赋值给原始数组。
在这里还有一个方法可以实现数组的响应式,就是之前提到的Vue.set()
Vue.set(vm._data_student_hobby, 4, "dd"),这样可以实现动态的在数组里面新增一个元素。
关于Vue监视数据,最后做一个总结:
1、Vue能够监视深层次的数据;
2、如何监视对象中的数据:
通过get和set并且要在new Vue时传入要监视的数据
(1)对象后面添加的属性,Vue默认不做响应式
(2)如果需要给后面添加的属性做响应式,就要使用API:
Vue.set(target, prop, value)或 this.$set(target, prop, value)
3、如何监视数组中的数据:
通过包裹数组更新元素的方法,来触发视图更新,本质上就是两步:
(1)调用数组的原生方法,对原数组进行更新;
(2)重新编译模板,视图更新。
4、Vue中修改数组的某个元素一定要用以下方法:
(1)push、pop、shift、unshift、splice、sort、reverse
(2)Vue.set()或者this.$set()
特别注意,Vue.set不能给Vue实例或者Vue实例的根数据元素添加属性!!!
数据劫持就是将data里面原来的所有属性,都佩戴上了get和set方法。
理解数据劫持和数据代理的点是:数据劫持监测到了数据变化,get和set就会将这个操作拦下来,做两件事:
1、先去正常改数据
2、然后触发视图更新
而数据代理只有改变数据,触发视图更新的操作是传递给了数据劫持来做。
Vue2.x的响应式
实现原理:
对象类型:通过Object.defineProperty()对属性的读取、修改进行拦截;
数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)
存在问题:
新增(Vue.set())、删除属性(Vue.delete()),界面不会更新。
直接通过下标修改数组(Vue.set()/原生数组方法),界面不会自动更新
Vue2.x中,要想删除对象上的属性,使用delete,需要在定义属性的时候配置configurable:true
这样就可以删除了,但是这个操作不是响应式的,因为我们在定义属性的时候只给它配置了get和set方法,删除属性时,是不会触发set方法导致视图更新的。如下所示:
let data = {
num: 1,
student: {
name: "aa",
age: 19,
address: "成都"
}
}
function Observe(obj) {
Object.keys(obj).forEach((key) => {
// 数据代理,目的是将_data上的属性添加到vm对象上
Object.defineProperty(this, key, {
configurable: true,
get() {
return obj[key];
},
set(newVal) {
console.log("视图更新啦~~");
obj[key] = newVal;
}
})
})
}
let obs = new Observe(data);
let vm = {};
vm._data = obs;
vm._data = data = obs;
delete data.num;
console.log(obs);
在上面这段代码中,确实num属性被删除了,但是set方法没有被触发,页面不会更新。
Vue3.0的响应式原理
(Vue2.x的以上问题Proxy都能够规避)
实现原理:
通过Proxy代理:拦截对象中任意属性的变化。包括对属性的增删改查
通过Reflect反射:对被代理的对象的属性进行操作。
设计思路:
一般对一个给定对象,我们要获取对象里面的属性,用obj.xxx就能获取。但是ECMA这个脚本语言,有意将Object上面的一些方法转移到Reflect上,在Vue的响应式实现中,就采取的Reflect来对对象进行操作。
Reflect的一个好处是:
比如,当使用Object.defineProperty()重复定义一个属性时,控制台会报错,js作为单线程语言,就会停止执行后面的代码。因此,为了避免这种现象,提高代码的健壮性(不因为出现错误了就停止当前代码的执行)就需要用到try/catch结构来规避这种错误,让代码能够继续执行下去。这样会造成一个结果就会最终我们的代码里面会有很多try/catch结构。因此,就推荐使用Reflect。
Reflect对象上定义了很多方法,它修改了Object对象某些方法的返回结果,比如defineProperty,在Reflect对象上调用defineProperty方法时,会有一个返回值,当在对象上重复定义一个属性时,会返回一个布尔值,表示当前的操作是否成功。如果重复定义了属性,那么该操作就返回false,并不会影响后续的操作。
let obj = {
name: "aaa",
age: 18
}
Object.defineProperty(obj, "sex", {
get() {
return "男"
}
})
// Uncaught TypeError: Cannot redefine property: sex
Object.defineProperty(obj, "sex", {
get() {
return "女"
}
})
console.log("执行到这里啦~~"); // 不执行
const x = Reflect.defineProperty(obj, "sex", {
get() {
return "男";
}
})
console.log(x); // true
const y = Reflect.defineProperty(obj, "sex", {
get() {
return "男";
}
})
console.log(y); // false
console.log("执行到这里啦~~"); // 执行到这里啦~~