Vue数据响应式主要研究的是 Vue 构造选项中 data 属性的特性
深入响应式 官方文档 网址: https://cn.vuejs.org/v2/guide/reactivity.html
1. 首先理解 ES6 的 getter 与 setter 语法
obj = { // 首先创建一个普通对象
name:'小欧',
age:18,
姓名(){
return this.name;
}
}
console.log(obj.姓名()); // 小欧
// 如何使 obj.姓名 不带括号也可以得到 name 值呢?
// 使用ES6新语法
obj = {
name:'小欧',
age:18,
get 姓名(){ // get (读) 一个值时执行的函数 获取时无需使用()执行
return this.name;
}
}
console.log(obj.姓名); // 小欧
obj = {
name:'小欧',
age:18,
get 姓名(){
return this.name;
},
set 姓名(set_name){ // 同样 set (写) 一个值得时候也能执行相应的函数
this.name = set_name + '你好'
}
}
console.log(obj.姓名); // 小欧
obj.姓名 = '高圆圆'
console.log(obj.name) // 高圆圆你好
2.再看 data 属性
当我们在vue实例创建之前定义一个数据对象,在没有经过 vue 实例化之前,是一个正常的对象
let tata = {
n:0
}
console.log(tata) // n:0 __proto__: Object
当我们使用该对象充当 Vue 实例的data属性之后
new Vue({
// components:{Demo},
template:`
`,
methods:{
des(){
this.visible = !this.visible
},
fn(){
console.log('调用了fn')
},
add(){
this.n++;
}
},
data:tata // 把自定义的对象充当 data 使用
}).$mount('#app')
console.log(tata) // 再次打印 tata 对象 如下图
数据 n 此时是两个函数的名字 通过 n 的获取或者赋值可以执行相应的函数,n:(...) 表示实际上不存在 n 这个属性了 只不过可以通过 get set 来模拟对 n 的取值 和赋值,在取值和赋值时完成更加灵活的操作(比如更新视图)
Object.defineProperty() 方法
该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42, // 表示此属性值 为 42
writable: false // 表示此属性值 不可写
});
object1.property1 = 77;
// throws an error in strict mode
console.log(object1.property1);
// expected output: 42
还可以设置 getter setter 属性
ojb3 = {
name:'小欧',
age:18
}
let _look = null;
Object.defineProperty(ojb3,'look',{
get(){
return _look
},
set(value){
_look = value
}
})
ojb3.look = '好看'
console.log(ojb3.look)
总结:关于 Object.defineProperty() 方法
- 可以给对象添加属性value
- 可以给对象添加getter / setter
- getter / setter用于对属性的读写进行监控
在 Vue 实例创建的时候,Vue会将 data 数据变为添加了getter setter 的数据属性
3.模拟 vue 的数据代理原理
需求一:
// 需求1:使用Object.defineProperty 定义 n
let data1 = {} // 空对象
Object.defineProperty(data1,'n',{
value:0
})
console.log(data1.n) // 0
需求二:
// 需求二 n 的值不能被赋值为小于零的数
// 比如 data2.n = 1 有效 data2.n = -1 无效
let data2 = {}
data2._n = 0 // 使用_n来储存n的值
Object.defineProperty(data2,'n',{
get(){
return this._n
},
set(value){
if(value < 0) return
this._n = value
}
})
data2.n = -1
console.log(data2.n); // 0 因为-1的赋值无效
data2.n = 1
console.log(data2.n); // 1 大于0有效
但是需求二中 如果直接修改 data2._n 属性也是无法拦截的,因此还要改进
需求三:使用代理模式
// 需求三:使用代理
let data3 = proxy({data:{n:0}})
function proxy({data} /* 解构赋值 */){
const obj = {}
// 这里的n理论上应该遍历data的所有key,这里简化了
Object.defineProperty(obj,'n',{
get(){
return data.n
},
set(value){
if(value < 0) return
data.n = value
}
})
return obj // obj就是代理对象
}
data3.n = -1 // 这里触发了data3 的 setter 函数 不会赋值为负数
console.log(data3.n) // 0
上面这个代理如果 proxy函数的参数不是匿名对象那还是会被修改值
需求四:使用代理的加强版
// 需求五 用户修改原始对象也能拦截
let myData = {n:0}
let data5 = proxy({data:myData})
function proxy({data}){
// 这里的n理论上应该遍历data的所有key,这里简化了
let value = data.n
delete data.n // 这行可以不写 因为下面创建的n属性会被覆盖
Object.defineProperty(data,'n',{
get(){
return value
},
set(newValue){
if(newValue < 0)return
value = newValue
}
})
// 上面这几句会监听 data 对象数据的变化
const obj = {}
Object.defineProperty(obj,'n',{
get(){
return data.n
},
set(){
if(value < 0)return
data.n = value
}
})
return obj
}
myData.n = -5
console.log(myData.n); // 0 被监听拦截
data5.n = -6
console.log(data5.n); // 0 被代理拦截
综上所述,vue实例创建时对 data 的篡改就包含了这个原理
let data5 = proxy({data:myData}) // 类似于
const vm = new Vue(data:{n:0}) // Vue对于数据的篡改原理正是上面解释的那样
小结:
vm = new Vue(data:{n:0})
// Vue会做出如下几件事
- 会让 vm 成为myData的代理 (proxy)
- 会对myData的所有属性进行监控
- 监控的目的就是在myData数据更改的时候,通知vm实例对象从而调用render方法更新视图
- 这便是Vue的看家本领
简言之就是 Vue通过 Object.defineProperty 对实例对象中data进行添加 getter / setter 从而用来对属性的读写进行监控,从而在data数据变化时通知Vue实例进行视图的更新
4.Vue data属性存在的问题
-
Object.defineProperty的问题
Object.defineProperty(obj, 'n',{...}) // 必须要有一个 n 属性才能监听
如果没有写 n 的话 Vue会给出警告,无法监听 后面添加的属性
new Vue({
// components:{Demo},
template:`
{{obj.b}}
`,
methods:{
add(){
this.obj.b++ // 变量 b 并没有提前写好 页面中就不会显示
}
},
data:{
obj:{
a:0 // obj.a 会被Vue监听 & 代理
}
}
}).$mount('#app')
解决办法:
使用 Vue.set() 方法的作用
- 新增 key
- 自动创建代理和监听(如果没有创建过)
- 触发视图更新(但并不会立刻更新)
new Vue({
// components:{Demo},
template:`
{{obj.b}}
`,
methods:{
add(){
Vue.set(this.obj,'b',1) // 使用Vue.set()方法添加监听属性
},
add2(){ // 点击按钮时 会更新视图
this.obj.b++
}
},
data:{
obj:{
a:0, // obj.a 会被Vue监听 & 代理
b:undefined
}
}
}).$mount('#app')
-
数组的篡改该方法
new Vue({
// components:{Demo},
template:`
{{obj.array}}
`,
methods:{
add(){
this.obj.array.push('d')
console.log(this.obj.array)
}
},
data:{
obj:{
array:['a','b','c']
}
}
}).$mount('#app')
打印结果:
为了防止每次修改数组都需要给后添加的数组项使用Vue.set(),数组传给Vue时,数组的这七个方法会被篡改覆盖,文档中叫做变异方法,这些方法会自动对数组新增项添加对应的监听,并且会更新视图
数组篡改的实现
所谓的方法篡改实际上就是在Vue实例上面加了一层原型链,同名的方法会被最底层的原型覆盖掉,就实现了方法篡改
ES6写法:以 push 方法为例 (模拟实现,并非源码)
class VueArray extends Array{
push(...arsgs){
const oldLength = this.length // this就是当前数组
super.push(...arsgs)
console.log('我被篡改了')
for(let i = oldLength; i < this.length; i++){
Vue.set(this,i,this[i]) // 将每个新增的 key 都告诉 Vue 实例
}
}
}