深入理解Vue数据响应式

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 对象 如下图
vue_get_set.png

数据 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() 方法

  1. 可以给对象添加属性value
  2. 可以给对象添加getter / setter
  3. 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会做出如下几件事
  1. 会让 vm 成为myData的代理 (proxy)
  2. 会对myData的所有属性进行监控
  3. 监控的目的就是在myData数据更改的时候,通知vm实例对象从而调用render方法更新视图
  4. 这便是Vue的看家本领

简言之就是 Vue通过 Object.defineProperty 对实例对象中data进行添加 getter / setter 从而用来对属性的读写进行监控,从而在data数据变化时通知Vue实例进行视图的更新

4.Vue data属性存在的问题

  1. 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() 方法的作用

  1. 新增 key
  2. 自动创建代理和监听(如果没有创建过)
  3. 触发视图更新(但并不会立刻更新)
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')
  1. 数组的篡改该方法
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')

打印结果:

数组的篡改方法.png

为了防止每次修改数组都需要给后添加的数组项使用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 实例
        }
    }
}

你可能感兴趣的:(深入理解Vue数据响应式)