简明Vue数据响应式教程

360 阅读2分钟

前言

数据响应式是Vue非常具有代表性的特点,在开发者修改数据后,Vue会根据数据来更新网页视图,这就给开发者提供了简单直接控制视图的工具。 不过这里面有一些小坑,知道一点原理,可以帮助我们少写一些bug。

什么是Vue的数据响应式?

上代码: const vm = new Vue({data:{n:0}})

如果修改了vm.n,那么网页UI中的n就会变化。Vue是通过Object.defineProperty来实现数据响应式的。

这里面其实有三个步骤

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

gette/setter

gettersetter可以设置在对象上,并对其数据进行读写。

先看代码:

let obj1 = {
  firstName: "小",
  lastName: "白菜",
  get fullName() {
    return this.firstName + this.lastName;
  },
  age: 18
};
console.log(obj2.fullName);
//输出结果:小白菜
//getter可以不加括号调用函数
let obj2 = {
  firstName: "大",
  lastName: "铁柱",
  get fullName() {
    return this.姓 + this.名;
  },
  set fullName(xxx){
    this.姓 = xxx[0]
    this.名 = xxx.slice(1)
  },
  age: 18
};
obj2.fullName = '大铁柱'
console.log(`姓 ${obj3.姓},名 ${obj3.名}`)
//需求二:需求二:姓 大,名 铁柱 
//getter用 fullName = xxx 来触发函数

一句话总结:

  • getter 就是不加括号的函数,使用 obj2.fullName 可以直接调用;
  • setter 接受一个参数, 用 fullName = xxx 来触发函数,并改变里面的值。

以上代码我们可以看出 gettersetter 都是声明时直接使用的,但我想声明后使用怎么办?接下来就轮到 Object.defineProperty() 出场了。

Object.defineProperty()

极简语法:Object.defineProperty(需要定义的对象, 定义的东西是什么, {getter/setter})

Object.defineProperty()的作用有三个:

  1. 给对象添加属性value;
  2. 给对象添加getter/setter;
  3. getter/setter用于监听属性的读写。

配合代码食用更好理解:

let data = {}
data._n = 0 // _n 用来存储 n 的值
Object.defineProperty(data, 'n', {
  get(){
    return this._n
  },
  set(value){
    if(value < 0) return
    this._n = value
  }
})
这里需要注意的是n是不存在的,并且不能直接通过get()来传递,否则会造成死循环。所以需要重新定义一个值来传。
但是,data._n暴露在外面,可以直接进行修改,这不是我们想要的,那么应该怎么做呢?
这里就需要用到一个代理了。
什么是代理?如何实现?
同样,先看代码理解:
let data0 = proxy({ data:{n:0} }) // 括号里是匿名对象,无法访问
function proxy({data}/* 解构赋值 */){
  const obj = {};
  Object.defineProperty(obj, 'n', { 
    get(){
      return data.n
    },
    set(value){
      if(value<0)return
      data.n = value
    }
  })
  return obj // obj 就是代理
}

以上代码块中存在解构赋值,原始代码如下:

function proxy(options){
  const {data} = options //这里也是解构
function proxy({data}){}  //这样
}

这样一来,如果对方想修改数据,只能通过obj来进行修改。

其实想要修改数据,还有另一种方法。只需要把匿名函数中的对象提取出来,并赋值给另一个变量即可:

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

这时候,我们不仅仅需要依靠代理,还需要再加一个监听。就算修改了myData,也可以拦截:

let myData = {n:0}
let data0 = proxy2({ data:myData }) // 括号里是匿名对象,无法访问
function proxy2({data}){
  let value = data.n //把data.n复制到value上
  delete data.n //这句可以省略,因为下面的语句会直接覆盖data.n
  Object.defineProperty(data, 'n', { //这里挂载了data
    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 // obj 就是代理
}

以上代码相当于把原来的数据复制了一遍,然后删掉旧数据,用新数据来填补,这样就可以防止通过对象来修改数据了。 接下来就容易理解我们在创建一个 Vue 实例时,数据响应式是如何工作的了。

new Vue做了什么?

对比一下这两个代码,是不是有内味了?

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

而在传入data的时候,Vue到底做了点啥呢?来看代码:

const Vue = window.Vue
const myData = {
  n:0
}
new Vue({
  data: myData,
  template: `
    <div>{{n}}</div>
  `
}).$mount('#app') // 挂载到index.html里面的div节点上
setTimeout(() => {
  myData.n += 10
}, 0)
console.log(myData)

控制台打印结果如下:

{__ob__: we}
     n: (...)
     __ob__: we {value: {...}, dep: ce, vmCount: 1}
     get n: f ()
     set n: f (t)
     __proto__: Object

在以上代码中,vm = new Vue({data: myData})能让vmmyData的代理,监控myData所有属性。打个比方,vmmyData类似于房东和中介的关系。

vm监听myData的变化,主要用于调用render(data),让UI变化,重新渲染网页。

总结

  • getter和setter可以设置在对象上,并对其数据进行读写;
  • Object.defineProperty()是Vue实现数据响应式的主要方式;
  • 为了不让外部直接修改对象参数,Vue需要给对象添加代理和监听;
  • Vue让vm成为myData的代理,开发者对vm的操作就是对myData的操作,并且还能通过this来访问vm;
  • Vue需要监听myData属性,一旦有变就会即时响应并刷新UI。