理解Vue数据响应式

1,218 阅读5分钟

什么是数据响应式

Vue.js 一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据。——《Vue.js 技术揭秘》

拿Vue官方教程举例来说,在Vue中我们只需要改变data中的price,页面会立马自动做出响应来显示更新后的数据。而不是像使用原生JS需要许多DOM操作才能完成。Vue是怎样监听到数据的改变让视图立马做出响应的呢?

上图是Vue的官方文档中的图解,黄色部分是 Vue 的渲染方法,视图初始化和视图更新时都会调用render 方法进行重新渲染。渲染时不可避免地会 touch 到每个需要展示到视图上的数据(紫色部分),触发这些数据的 get 方法从而收集到本次渲染的所有依赖。而当我们在修改这些收集到依赖的数据时,会触发数据中的 set 属性方法,该方法会修改数据的值并 notify 到依赖到它的观察者,从而触发视图的重新渲染。

而我们定义在data中的数据并没有set,get的计算属性,get、set方法是如何产生的呢?这便是Vue的数据响应式的核心工作,重写数据的 get 和 set 属性方法。

让数据变成响应式

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。

上面是Vue官方文档的解答,下面详细了解一下Vue如何将数据变为响应式的?

getter、setter

let obj = {
firstName: "王",
lastName: "XX",
get name() {
return this.firstName + this.lastName;
},
set name(value){
this.firstName = value[0]
this.lastName = value.slice(1)
},
age: 18
};

obj.name = '王小小'
console.log( obj.name);

如上例,getter、setter方法也可以是obj对象的属性,我们可以使用操作属性的点语法来获取name和修改name。如果在控制台进行打印就会发现 name属性下的getter、setter方法就和实例化vue中的data 里的值n打印出来变为了n:(...)也有getter、setter方法,说明vue重写数据n,将其转换为getter/setter的对象属性。

Object.defineProperty

Vue是如何将data中普通的属性n转化为getter、setter方法的属性n呢?答案是通过 JS 标准内置对象方法 Object.defineProperty 来设定的

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。——MDN

Object.defineProperty(obj, prop, descriptor)

Object.defineProperty() 接收三个参数:第一个是要定义属性的对象;第二个是要定义或修改的属性的名称或 Symbol;第三个则是要定义或修改的属性描述符。在第三个参数中,我们可以定义属性的 getter 函数和setter 函数,这便解释了Vue如何通过Object.defineProperty 把这些 data里的属性全部转为 getter/setter。

var obj = {}; // 创建一个新对象
// 在对象中添加一个属性a
Object.defineProperty(obj, "a", {
  get() { return aValue; },
  set(newValue) { aValue = newValue; },
});

getter和setter对属性的读写操作进行了监控,但是我们只要知道属性名,就可以绕过getter和setter方法直接对属性进行控制,Vue利用代理模式解决了这个问题

proxy

let myData = {
    n: 0
}
let data = proxy({
    data: myData
}) // 括号里是匿名对象,无法访问
function proxy({ data } /* 解构赋值 */ ) {
    //for循环省略
    let value = data.n
    Object.defineProperty(data, 'n', {
        get() {
            return value
        },
        set(newValue) {
            value = newValue
        }
    })
    
    const obj = {}
    Object.defineProperty(obj, 'n', {
        get() {
            return data.n
        },
        set(value) {
            data.n = value
        }
    })
    return obj // obj 就是代理
}

上面的方法对每个传入的数据新增 getter/setter,此后原始的数据就会被 getter/setter 所替代,相当于复制了原始数据,这样不管是操作 let data = proxy(data); 中的 data,还是操作 myData,都会被我们的 getter/setter 所拦截。

经过代理后的代码是不是就和实例化一个vue对象的代码很相似了

const vm = new Vue({ data: {} })
let data = proxy({data:myData})

所以,new Vue时Vue会遍历传入的data对象所有属性,并使用Object.defineProperty把这些属性全部转为getter/setter,这样就生成一个新的对象全权负责数据——就是实例化的Vue对象vm。这样vm会成为data 的代理,对 data 的所有属性进行监控,当数值发生改变的时候,vue就调用render函数重新渲染视图。

Vue 数据响应式的 Bug

由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。——Vue文档

是什么?

var vm = new Vue({
  data:{
    a:1
  }
})
// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 是非响应式的

由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。而例子中的b属性是在实例化外再新添的,所以vue不能对b值的变化在视图上做出响应。但很多时候我们并不能事先知道data的所有属性,如果要新添属性怎么办呢?

解决方法

对象

可以使用 Vue.set(object, propertyName, value) 方法向对象添加响应式 property。

Vue.set(vm.data, 'b', 2)

还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:

this.$set(this.data,'b',2)

数组

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c','d']
  }
})

88e18f6ef74cb72fc36ad38fdbeaced.png

把数组打印出来可见,vue对数组进行了改变,给数组加了一层原型,在其中Vue修改了7个方法覆盖了之前数组原型的7个方法。调用这些Vue新定义的方法时,在这些新方法里Vue会加上对新添的元素的监听(相当于进行了set操作),把新数据也进行代理,这样vue就能重新监测到数组的变化了更新UI操作

具体的七个变更方法:(这些原始方法正好也会返回一个新数组)

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

关于Vue的响应式原理,还有很多需要探究的,这篇文件只是总结了数据如何变为响应式的。

参考

深入响应式原理

Vue的数据响应式原理

一探 Vue 数据响应式原理