Vue到底对 data 做了什么?
import Vue from 'vue/dist/vue.js'
Vue.config.productionTip = false
const myData = {
n: 0
}
console.log(myData)
const vm = new Vue({
data: myData,
template: `
{{n}}
`,
methods: {
add(){
this.n +=10
}
}
}).$mount("#app")
setTimeout(() => {
this.n += 10
console.log(myData)
}, 3000)
- 我们有如上的代码,其中 data 中的的数据引用外面的变量 myData;
- 我们在两处进行打印,一处是初始化的时候打印,一处是在变化之后进行打印;
-
我们可以看到初始化的时候打印这个myData就只是我们定义的对象,但是在变化之后再打印这个对象就会发生变化,好像是被包裹了一层东西;
- 一开始是 {n:0},传给 new Vue 之后就立马变成{n:(...)};
- {n:(...)}是个什么东西,为什么变现和 {n:0}一致?
- 我们了解这个知识点需要知道什么是 ES6 的 getter 和 setter。
getter 和 setter
- 我们声明一个变量,这个变量的属性如下,具有一个姓的属性,一个名的属性,还有一个age属性;
let obj0 = { 姓: "高", 名: "圆圆", age: 18 };
- 需要一:得到姓名,那么只要在里面添加一个方法就好了,在对象中,属性名和属性值一样的时候,是可以直接写函数的。
let obj1 = { 姓: "高", 名: "圆圆", 姓名() { return this.姓 + this.名; }, age: 18 }; console.log("需求一:" + obj1.姓名())
- 但是这个函数我们需要调用,也就是后面又括号,那么怎么去掉括号呢?
- 需求二:姓名之后不加括号也能得出值
- 这个需要使用 ES6 的新语法,get方法
let obj2 = { "姓": "高", "名": "圆圆", get 姓名() { return this.姓 + this.名; }, age: 18 } console.log("需求二:" + obj2.姓名)
- 我们使用 get 之后发现,这个姓名是一个属性,不用加括号,只不过是以函数形式定义的,看起来跟普通的属性并没有区别
- 这种写法是 getter,用于获取这个值
- 需求三:姓名可以被写,这个写法是 setter
let obj3 = { "姓": "高", "名": "圆圆", get 姓名() { return this.姓 + this.名; }, set 姓名(xxx){ this.姓 = xxx[0] this.名 = xxx.slice(1) }, age: 18 } obj3.姓名 = '刘诗诗' console.log(`需求三:姓 ${obj3.姓}, 名 ${obj3.名}`) console.log(obj3)
- 我们将这个obj3打印出来看看
-
- 这个姓名其实跟我们之前的那个data中的数据是一样的形式,说明我们之前的那个数据也是getter和setter形成的
- 这儿的姓名不是一个真实的属性,但是我们可以对这个属性进行读和写,读写通过getter和setter进行操作
Object.defineProperty
- 我们之前在使用getter和setter的时候是在定义的时候直接使用的,当我们在定义好一个对象之后,无法再添加get和set。
- 但是如果我们想在后面写怎么办?
- 这个时候我们就需要使用Object.defineProperty
- 下面是一个示例代码
let obj3 = { "姓": "高", "名": "圆圆", get 姓名() { return this.姓 + this.名; }, set 姓名(xxx){ this.姓 = xxx[0] this.名 = xxx.slice(1) }, age: 18 } var _新属性 = 0 Object.defineProperty(obj3, '新属性', { get(){ return _新属性 }, set(value){ _新属性 = value } })
- 上述代码中,我们想要添加一个新属性,其中_新属性是为了盛放新属性的值
- 我们定义的属性是不存在的,所以在get和set方法中不可以使用本属性,因为新属性的使用是通过get和set方法的,如果在get和set中使用就会造成死循环
- 这个_新属性其实是一个代理
代理和监听
- 需求一:用 Object.defineProperty 定义 n
let data1 = {} Object.defineProperty(data1, 'n', { value: 0 }) console.log(`需求一:${data1.n}`)
- 我们定义了一个data1,想要添加一个属性,并设置成0
- 需求二:n不能小于0
let data2 = {} data._n = 0 Object.defineProperty(data2, 'n', { get(){ return this._n // _n用来存储n的值 }, set(value){ if(value < 0) return this._n = value } }) console.log(`需求二:${data2.n}`) data2.n = -1
- 我们使用了一个临时的变量_n去存储
- 如果对方直接使用这个data._n呢?
- 我们怎么将这个data._n将其变得访问不到呢?
- 可以使用代理
- 需求三:使用代理
let data3 = proxy({ data: {n:0} }) // 括号里面的匿名对象无法访问,因为没有名字 function proxy({data}){ const obj = {} // 这里的 n 写死了,理论上应该是遍历属性 Object.defineProperty(obj, 'n' { get(){ return data.n }, set(value){ if(value<0)return data.n = value } }) return obj // obj就是代理 } console.log(`需求三:${data3.n}`) data3.n = -1
- 函数传进去的匿名对象是无法访问的
- 函数里面使用的obj是一个代理
- 我们将data3中的所有属性值通过get和set的方式转给代理obj
- data3理论上跟obj是等价的,我们通过在obj里面去设定一些机制,防止外界随意篡改,如不可以小于0
- 代理的作用就是做一些限制保护,只要原始传进去的对象不暴露在外面就行。
- 但是如果对于一些个变量已经暴露在外面怎么办?
- 需求四:就算用户擅自修改myData,也要拦截它
let myData = {n:0} let data4 = proxy({ data: myData }) myData.n = -1
- 上面这个代码就直接绕过了代理,修改了最原始的myData
- 我们通过下面的代码实现了怎样能在用户修改的时候做一个监听呢?
let myData5 = {n:0} let data5 = proxy2({ data: myData5 }) // 这个改进的代码可以100%的防止超出规则的数据更改 function proxy2({data}){ // 使用这个value存储原始的data中的n,先将其记下来,因为待会要删除这个n let value = data.n // 我们创建一个新的n就是覆盖原先的n,覆盖就相当于删除 // 这个部分是监听逻辑,只要n发生更改,就出触发这个,但是返回值还是需要通过判断才能更改的 // 我们将原先的属性删掉,通过get和set获取以及重置,这样就可以在其中设置规则,即监听 Object.defineProperty(data, 'n', { get(){ return value }, set(newValue){ if(newValue<0)return value = newValue } }) // 这部分是代理逻辑 const obj = {} Object.defineProperty(obj, 'n', { get(){ return data.n }, set(value){ if(value<0)return data.n = value } }) }
- 上面这串代码改写的代理函数分为两部分,一部分是负责监听,另一个部分是用于代理;
- 负责监听的那部分通过使用闭包,拿到数据就在内部存储一下数据,并通过get和set的方式重新定义同名数据,这样外界在访问的时候就必须通过这个get和set,在这个里面做一些限制,就可以实现监听效果
- 代理逻辑还是跟原先的一样
- let data5 = proxy2({ data: myData5 })这段代码跟const vm = new Vue({data: {n: 0}})很是类似
那么 Vue 到底对数据做了什么?
- 啥是代理?(设计模式)
- 对 myData 对象的属性读写,全权由另一个对象 vm 负责(通过将myData中的属性用get和set的方式给vm)
- 那么 vm 就是 myData 的代理
- 比如 myData.n 不用,偏要用 vm.n 来操作 myData.n
- 有关 vm = new Vue({data: myData})
- 会让 vm 成为 myData 的代理(proxy)
- 会对 myData 的所有属性进行监控
- 为什么要监控?为了防止 myData 的属性变了,vm不知道
- vm 知道了又如何?知道属性变了就可以调用 render(data)
- UI=render(data)
-
- 全程最开始的对象是不变的,我们操作和展示的时候,只动vm中的对象
什么是数据响应式
- 什么是响应式?
- 如果一个物体对于外界的刺激能做出反应,他就是响应式的
- Vue 的 data 是响应式
- const vm = new Vue({data: {n: 0}})
- 我如果修改 vm.n,那么 UI 中的 n 就会响应我
- Vue 2 通过 Object.defineProperty 来实现数据响应式
Vue 的 data 的 bug
- Object.defineProperty 的问题
- Object.defineProperty(obj, 'n', {...})
- 必须要有一个 'n',才能监听&代理obj.n
- 如果前端工程师没有给出n怎么办?
- 情况一:第一层属性没有n,Vue会给出一个警告
-
- 会警告并没有这个属性
-
- 情况二:属性是一个对象,对象里面有属性,这种情况下,Vue只会检查第一层属性
- 属性里面的属性如果不存在,就无法进行后续操作的
-
-
- 点击之后,并不会显示b的值
- 为什么:因为Vue无法监听一开始就不存在的obj.b
- 解决方法:
- 一开始就将需要用到的key监听好,如b: undefined
- 使用 Vue.set 或者 this.$set
- this.$set(this.obj, 'b', 1)
- Vue.set(this.obj, 'b', 1)
Vue.set 和 this.$set
- 作用
- 新增 key
- 自动创建代理和监听(如果没有创建过)
- 触发UI更新(但并不会立刻更新)
- 举例
- this.$set(this.object, 'm', 100)
数组的变异方法
- 我们知道数组最终是一个对象;
- 如a = ['a', 'b', 'c'],实际上会是a = {0: 'a', 1: 'b', 2: 'c'}
- 这样在 vue 中,当我们需要给数组添加元素的时候,不可以直接使用原先的this.a[3] = "d"这样的方法,因为vue对原先不存在的数据是无法进行监听的,需要使用this.$set()
- 这样子,我们对于数组的所有操作,都得使用$.set?
-
使用 this.a.push('d'),这个方法经过Vue改编了,同名,可以支持在vm中使用,就是在原先的方法那边添加一个set
- 篡改的方式是添加一层原型,在原型上面篡改