Vue数据响应式原理

564 阅读4分钟
Vue中的model对象被整合到了Vue实例vm的data属性中,数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。所以本文就data的更新原理做一个解析。详情见Vue官方文档

一、 \color{ #e917c8}{如何追踪data变化}

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter
那么什么是getter/setter呢,简单代码说明一下
var o = {
  a: 7,
  get b() { 
    return this.a + 1;
  },
  set b(x) {
    this.a = x / 2
  }
};
我们为o中的函数b设置一个set和一个get,这样会得到以下这个对象o

可以看到b这时候是一个不确定的状态,这是因为并不存在一个叫b的属性,我们要通过get b()或者get b()才能计算出他的值,点击一下他就会成为8

这时候我们给b赋值
o.b=20
//其实相当于这样o.b(20)

这时候a的值会随之改变,这时候被赋值的内容会被当做参数传入set b(x)

总结一下:b是一个定义的函数,但是在前面加上get和set以后,我们在调用这个函数的时候不用加()

使用Object.defineProperties的方法,同样也可以对一个已创建的对象在任何时候为其添加getter或setter方法。
第一个参数是你想定义getter或setter方法的对象
第二个参数是一个对象,这个对象的属性名用作getter或setter的名字,属性名对应的属性值用作定义getter或setter方法的函数
Object.defineProperties(o, {
    "b": { get: function () { return this.a + 1; } },
    "c": { set: function (x) { this.a = x / 2; } }
});

了解了上面的getter/setter以后我们还是不知道Vue是如何知道data变化的,所以下面介绍下Vue是如何监听data的

二、 \color{ #e917c8}{代理和监听}

我们先来看下如何利用setter来对data的变化做一个限制
let data1 = {}

Object.defineProperty(data1, 'n', {
  value: 0
})
简单的给一个对象赋值,注意value
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} 设置为 -1 失败`)
//n=0
在set中对传入的n做一个限制,如果它小于0,则不赋值给_n这个临时变量
\color{ #ff0000}{注:} 这里为什么要使用一个临时变量才存储n的值呢,因为如果直接将return n那么则会递归的去调用get()导致爆栈
但是如果我们直接改变data2._n的值呢,这时候set就不能起到过滤的作用了,因为我们的临时变量是完全暴露在外的
data2._n = -1
console.log(`${data2.n} 设置为 -1 失败`)
//n=-1
所以接下来我们想到了使用代理,将临时变量给隐藏起来,定义一个函数proxy
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
console.log(`${data3.},设置为 -1 失败`)
//n=0
此时我们使用的是一个匿名对象,所以无法对其属性进行修改
但是如果我们给这个匿名对象是一个引用呢
let myData = {n:0}
let data4 = proxy({ data:myData }) // 括号里是匿名对象,无法访问
myData.n = -1
//这里是直接给data中的n赋值了,他是通过get使得n改变的
//而上一次data3.n是通过obj的set赋值给n的所以被set过滤了
console.log(`${data4.n},设置为 -1 失败了吗!?`)
//n=-1
既然你想改掉我的源对象中的n,那么好,我把源对象的data变化设置一个监听
let myData5 = {n:0}
let data5 = proxy2({ data:myData5 }) // 括号里是匿名对象,无法访问

function proxy2({data}){
  let value = data.n
  //将原来的n赋值到value这里,然后把源对象n删除
  //delete data.n
  Object.defineProperty(data, 'n', {
    //这里的n和之前的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(value){
      if(value<0)return
      data.n = value
    }
  })
  
  return obj
}
由于我们把源对象myData的所有属性都复制到了value(例子中只有一个实际上是遍历data),然后把源对象删除,重新生成一个虚拟对象n,他只能通过get或者set访问,现在如果你还想改myData.n就无法绕过set的过滤
myData5.n = -1
console.log(`${data5.n},设置为 -1 失败了`)
//n=0
这里其实就可以窥见Vue内部对于data对象中的属性所进行的一系列操作,现在我们就可以对data的变化进行监听,并将变化后的值渲染进页面中,附上Vue源码中的proxy函数,代理之后对这个data进行监听(observe)

这个observer就是构造watcher实例的构造函数,这里的value参数就是data

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

如果data是一个数组那么会对数组遍历并对其进行getter/setter封装

这里就是getter/setter函数

做一个总结

1.对data对象进行改造,将源对象变成无法直接读取的getter/setter对象,并监听 2. 将这个对象设置一个代理对象,并暴露给外部进行访问,注意这里的data已经是改造后的data对象了

三、 \color{ #e917c8}{添加响应式属性data}

对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性,这是因为Vue只检查第一层的对象是否定义,因此我们可以在已定义的对象上嵌套其他响应式属性
new Vue({
  data: {
    obj: {
      a: 1 // obj.a 会被 Vue 监听 & 代理
    }
  },
  template: `
    <div>
      {{obj.b}}
      <button @click="setB">set b</button>
    </div>
  `
})
由于这里b没有定义在data中,所以我们可以用Vue.set(object, propertyName, value)或者vm.$set
methods:{
Vue.set(this.obj, 'b', 2)
this.$set(this.obj,'b',2)
}
有时你可能需要为已有对象赋值多个新属性,比如使用 Object.assign() 或 _.extend()。但是,这样添加到对象上的新属性不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的属性一起创建一个新的对象。
this.obj = Object.assign({}, this.obj, { a: 1, b: 2 })

如果我们的data中是一个数组怎么办

new Vue({
  data: {
    obj: {
      array:["a","b","c"]
    }
  },
  template: `
    <div>
      {{obj.b}}
      <button @click="setD">set d</button>
    </div>
  `,
  methods:{
      setD(){
          this.array.push("d")
      }
  }
  
})
这里的push是Vue二次封装的产物,在array被传入之后,他拥有了额外第一层原型链,这层原型上有适合Vue的数组操作方法

<===TO BE CONTINUED