对 Vue 数据响应式的理解

370 阅读5分钟

Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。对这些对象进行操作时,却能影响对应视图,核心实现就是「响应式系统」。尽管我们在使用 Vue.js 进行开发时不会直接修改「响应式系统」,但是理解它的实现有助于避开一些常见的「坑」或通过原理解决问题。

先了解getter和setter:

getter:

let obj1 = {
firstName:"高",
lastName:"圆圆",
姓名(){
return this.firstName + this.lastName;
}}
console.log("打印一:" + obj1.姓名()); 
// "打印一: 高圆圆

加了get
let obj2 = {
firstName:"高",
lastName:"圆圆",
get 姓名(){
return this.firstName + this.lastName;
}}
console.log("打印二:" + obj2.姓名);   // 这里没有加括号了 
//打印二:高圆圆
总结:getter 就是不加括号的函数

setter:

let obj3 = {
firstName:"高",
lastName:"圆圆",
get 姓名(){
return this.firstName + this.lastName;
}
set 姓名(xxx){
 this.firstName = xxx[0]
 this.lastName = xxx.substring(1)
}}
obj3.姓名 = "刘诗诗"
console.log(`打印三: 姓${obj3.firstName},名${obj3.lastName}`) 
//打印三:姓:刘,名:诗诗
总结: setter就是接受一个参数的函数 用 = xxx 这样的形式触发,来改变里面的值

set和get后便是:

Object.defineProperty():

我们使用set和get的时候都是在声明的时候就直接使用的,但是我想声明之后再使用怎么办,这就用到Object.defineProperty()了

Object.defineProperty()第一个参数是告诉它,定义再那个对象上,第二个参数就是要定义个什么东西,然后就能写了

var _xxx = 0
Object.defineProperty(obj3,"xxx",{
 get(){
return _xxx
},
 set(value){
 _xxx = value
}
})

这里需要注意定义的xxx是不存在的所以你用get传是不行的,而且会死循环(因为你在调用xxx的时候就会触发,无限循环),所以需要重新定义一个值用来传。

但是这里有一个问题,我们可以通过直接改变全局变量_xxx来改变里面的值,这不是我们想看到的,那么如何解决这个问题呢?

那就需要一个代理了:

let data1 = proxy({data:{n:0}}) // 括号里面是匿名对象,所以根本无法访问

function proxy({data} /*这里用了解构赋值*/){
  conost obj = {}
  Object.defineProperty(obj,"n",{
     get(){
 return data.n
}
     set(value){
 if(value<0)return
 data.n = value
}
})
return obj //这就是代理
}

上面用到解构赋值,本来应该这样写 :

function proxy(options){
  const {data} = options //这里也是解构,太基础,不懂建议先去了解
  因为上面options只用了一次所以,直接参数哪里直接表示就行了:function proxy({data}){} 
}

这样你修改我obj的n我就会去设置data的n,obj就是一个代理,这样你就不能擅自修改我的对象数据,你只能通过obj代理来修改?

错! 我还是能修改

如果你那个匿名函数我用对象给你呢

let myData5 = {n:0}
let data1 = proxy({data:myData5})

那我只要修改myData5 还是能改你的数据,这时候就不能只靠代理了,还需要监听,这样就算你改了也没用

let myData5 = {n:0}
let data = proxy({data:myData5})

function proxy({data} /*这里用了解构赋值*/){

let value = data.n
dalate data.n   //这一句可以不写因为下面已经覆盖了原先的n
Object.defineProperty(data,"n",{ //这里挂载的对象就是参数data
     get(){
 return value
}
     set(newvalue){
 if(newvalue<0)return
 value = newvalue
}
})
//上面这几句,就会监听data
  conost obj = {}
  Object.defineProperty(obj,"n",{
     get(){
 return data.n
}
     set(value){
 if(value<0)return
 data.n = value
}
})
return obj //这就是代理
}

上面的方法就是把原先的数据复制一遍,再删掉用新数据来填补,这样你就不能通过对象修改我的数据,即:

let data = proxy({data:myData5})
const vm = new Vue({data:{n:0}})

new Vue()

vue 对 data 做了什么?

当你创建一个实例时

const vm = new Vue({data: myData})
  • vue 会让 vm 成为 myData 的代理。
  • vue 会对 myData 的所有属性进行监控。

目的

这样做的目的是什么?

  • 你可以使用 this 来访问到 vm。 this.n === myData.n。
  • 之所以要监控,就是防止 vue 无法得知 myData 的属性变化。
  • vue 得知属性变化才可以使用 render(data) 来更新 UI 和渲染页面。

数据响应式

  • 响应式即对外界的变化做出的反应的一种形式。
  • const vm = new Vue({data:{n: 0}})
  • 当修改 vm.n 或 data.n 时,render(data...) 中的 n 就会做出响应的响应。
  • 这个联动的过程就是 vue 的 数据响应式。
  • vue 目前通过 Object.defineProperty 来实现数据响应式。

如何追踪变化

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

Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

这些getter/setter对用户来说是不可见的,但是内部他们让Vue能够追踪依赖,在property被访问和修改时通知变更。

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

在 data 中添加属性

Vue 虽然对 data 中的属性(或对象中的属性)进行监听和代理,但是它却没有办法进行事先的监听和代理。

如果你在初始化 data 之后再添加属性,该如何实现?

一般对象

对于一般的对象来说,可以在 data 中预先把所有可能用到的属性全部写出来,这样并不需要新增属性,只需要改它。

也可以通过其他方法来添加属性。

在了解以上原理后,我们来了解 Vue 提供的一个 API:

Vue.set(object, key, value)

this.$set(object, key, value)

作用

  • 在 data 中添加新的属性。
  • 自动创建为它创建代理和监听(如果没有创建过)。

示例

const Vue = window.Vue

new Vue({
    data: {
        obj: {
            a: 0
        }
    },
    template: `
    <div>
      {{obj.b}}
      <button @click='one'>One</button>
    </div>
    `,
    methods: {
        one() {
            Vue.set(this.obj, 'b', 1)
            // 或 this.$set(this.obj, 'b', 1)
        }
    }
}).$mount('#app')

数组

因为数组本身的特殊性:数组的长度无法预测(比如所有用户的用户名,存在数组中),你无法使用 undefined 去为每一项占位,或一直使用 Vue.set( ) 方法。

  • 你可以使用 push 方法 this.array.push('value'),但其实数组已经被 Vue 包装了新的 push 方法。
  • 原理就是声明一个新的类来继承数组。
  • 各种在 Vue 实例 中使用的特例方法, 详见数组变异方法,一共有7个API。
  • 这些方法 (API) 会自动处理对数组该项的监听和代理,并触发视图更新。

什么才是最重要的 研究思路和方法永远比知识更重要。 你可以在不懂、不看源代码的前提下,去使用和理解一个库或项目或其他。 前提是你已经知道了它的思路和方法。