理解 Vue 响应式原理

221 阅读2分钟

响应两个字可以理解为:当 A 对 B 说话时,B 会根据 A 说话的内容进行回应。在 Vue 中也是一样,当 Vue 的数据模块发生改变时,视图根据已修改的数据重新渲染页面,这就是 Vue 的响应式原理。

一、Vue 如何知道数据被修改?视图又如何监听数据模块?

事实上,当我们将一个普通对象传到 Vue 的数据模块,Vue 会通过 Object.defineProperty 方法将对象所有的 Property 属性都转化为 getter/setter,并将原先的 Peoperty 属性覆盖掉。这样,读取数据需要经过 getter,修改数据需要经过 setter,从而使 Vue 可以对数据模块进行监听。

而视图主要通过 Watcher 对数据模块进行监听,当数据模块中的数据被修改或者访问时,Watcher 就会被监听到,然后调用 render 方法,重新渲染页面。

image.png

另外,在修改数据模块的数据时,如果需要设定条件限制,同时又要防止数据被篡改,应该如何实现这个功能?可以使用一个中间对象进行代理,拦截对原对象的任何交互,具体实现代码如下所示:

// 代理和监听
let sourData = {
    name: "frank",
    age: 18,
}  
let myData = proxy({
    data: sourData
})
function proxy({data}) {  // 解构赋值
    let value = data.age  // 拿到 data.age
    Object.defineProperty(data, "age", {
        get() {
            return value
        },
        set(newValue) {
            if (newValue < 0) return
            value = newValue
        }
    }) // 监听:age 会覆盖原来的 age,且第二次开始,age 的值只能通过 setter 更改,无法通过 sourData.age 更改。
    
    let obj = {}
    for (let key in data) obj[key] = data[key]
    Object.defineProperty(obj, "age", {
        get() {
            return data.age
        },
        set(value) {
            data.age = value
        }
    })
    return obj // 代理:obj 就是代理,如果不使用代理,即不使用代理函数 proxy() 时,myData 里面的 age 值可以通过赋值的方式更改。
}

console.log(myData)
sourData.age = -100
console.log(myData.age)  // 失败
myData.age = -200
console.log(myData.age) // 失败

二、在数据模块中添加或移除对象

Vue 无法检测 property 的添加或移除。如果数据模块中开始时没有添加某个属性,我们是无法对其进行监听的,就算后继通过普通的方法增加,也不起作用,需要使用 Vue.set(Object, propertyName,value) 方法或者 Vue 的实例方法 vm.$set 方法添加属性到数据模块。

var vm = new Vue({
  data:{
    a:1
  }
})

// `vm.a` 是响应式的

// 错误的添加方法。`vm.b` 是非响应式的
vm.b = 2

// 正确的添加方法
Vue.set(vm.data, 'b', 2)
vm.$set(vm.data,'b',2)

// 正确的删除方法
Vue.delete(vm.data, 'a')
vm.$delete(vm.data,'a')

三、修改数组的元素或索引

Vue 不能检测以下数组的变动:

  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如:vm.items.length = newLength

需要使用 setsplicepush 方法,代码如下:

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的

// 正确的方式
Vue.set(vm.items, indexOfItem, newValue)
vm.$set(vm.items, indexOfItem, newValue)
vm.items.splice(indexOfItem, 1, newValue)
vm.items.splice(newLength)

// 使用 push,数组的变更方法
vm.items.push(newValue)

参考资料