Vue响应式探究

682 阅读3分钟

官方定义:

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

注意事项:

Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

知识点与疑问:

  • Object.defineProperty可以获取到数据getter/setter变动
  • 版本支持:Vue 不支持 IE8 以及更低版本浏览器
  • property未定义的不能执行getter/setter转换,不能转化为响应式 (为什么?)
  • 数据响应式的关键是可以获取到数据的变化即getter/setter,也是前提

响应式原理

基于以上的基本信息来了解一下Object.defineProperty()这个API,同时来探讨上面疑问的原因

MDN定义:

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

get属性的 getter 函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。

set属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象

代码演示__创建、修改属性:

// 创建属性
let obj = {}
Object.defineProperty(obj,'name',{
  value: 'zhang san'
})
console.log(obj.name); // zhang san

// 修改属性
let obj = {name:'zhang san'}
Object.defineProperty(obj,'name',{
  value: 'li si'
})
console.log(obj.name); //li si

代码演示__获取property修改变动:

let obj = {name:'zhang san'}
analysis(obj,'name',obj['name'])
function analysis(obj,name,val) {
  Object.defineProperty(obj,name,{
    get(){
      console.log('get');
      return val
    },
    set(newValue) {
      console.log('set');
      if (newValue === val) {
        return
      }
      val = newValue
    }
  })
}
let a = obj.name //触发 get
obj.name = 'li si' //触发 set
console.log(obj.name); //触发get li si

基于上面的示例再次扩展,给obj对象新增一个age属性即obj.age,发现并未触发get、set函数。也回答了如果一个property未添加在Vue的data对象上,不会触发数据响应式。

let a = obj.age // 未触发get
obj.age = 18 // 未触发get

模仿实现Vue数据响应式实现

下面模仿实现创建一个Vue对象,简单实现一个Vue中的数据响应式。

注意点: Vue中获取、修改的数据的方式是 this.name, 并不是 this.data.name, 关于这点是因为用到了Object.defineProperty可以创建属性,那么这里的属性就是Vue的实例对象。 由于下面的代码过长,将会分段展示:

创建的一个单独的观察者类,来处理data对象的数据变化

// 创建一下观察者类
class Observe {
  constructor(data) {
    // 遍历对象中的数据
    Object.keys(data).forEach(key=>{
      analysis(data,key,data[key])
    })
  }

  analysis (obj,key,val){
    Object.defineProperty(obj,key,{
      get(){
        console.log('get');
        return val
      },
      set(newValue) {
        console.log('set');
        if (newValue === val) {
          return
        }
        val = newValue
      }
    })
  }
}

Vue中的相关处理以及数据打印

function Vue(options) {
  this.options = options
  this.data = options.data || {}
  // 给vue对象的上挂载数据
  this.proxyData(this.data)
  new Observe(this.data)
}

function proxyData(data) {
  Object.keys(data).forEach(key=>{
    Object.defineProperty(this,key,{
      get(){
        return data[key]
      },
      set(newValue) {
        if (newValue === data[key]) {
          return
        }
        data[key] = newValue
      }
    })
  })
}

let vm = new Vue({
  data: {
    name: 'zhang san',
    age: 18
  }
})
let a = vm.name
vm.name = 'li si'
console.log(vm.name);

Vue2.x VS Vue3.x 响应式方法对比

Vue3.x中响应性的方法改为使用ES6新增的特性:Proxy。官方也表示在Vue3中也保留了Object.defineProperty来支持 IE 浏览器。

Proxy示例:以下示例用到了数组:

const data = {
  list: [1]
}
const handler = {
  get(target, prop, receiver) {
    console.log('get');
    return Reflect.get(...arguments)
  },
  set(target, key, value, receiver) {
    console.log('set');
    return Reflect.set(...arguments)
  }
}
const proxy = new Proxy(data, handler)
proxy.list = [1,2] // 触发set
proxy.list[1] = 3 // 触发get