深入理解 Vue 响应式原理

460 阅读2分钟

介绍

Vue 通过 Object.defineProperty 的 getter/setter 对收集的依赖项进行监听,在属性被访问修改时通知变化,进而更新视图数据;

受现代JavaScript 的限制 (以及废弃 Object.observe),Vue不能检测到对象属性的添加删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化过程,所以属性必须在 options 的 data 对象上存在才能让Vue转换它,这样才能让它是响应的。

getter/setter

getter: get语法将对象属性绑定到查询该属性时将被调用的函数。

setter: 当尝试设置属性时,set语法将对象属性绑定到要调用的函数。

Object.defineProperty

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

Vue 做了哪些事情?

先来看一段代码:

let data0 = {
  n: 0
}

// 需求一:用 Object.defineProperty 定义 n
let data1 = {}

Object.defineProperty(data1, 'n', {
  value: 0
})

console.log(`需求一:${data1.n}`)

// 总结:这煞笔语法把事情搞复杂了?非也,继续看。

// 需求二:n 不能小于 0
// 即 data2.n = -1 应该无效,但 data2.n = 1 有效

let data2 = {}

data2._n = 0 // _n 用来偷偷存储 n 的值

Object.defineProperty(data2, 'n', {
  get(){
    return this._n
  },
  set(value){
    if(value < 0) return
    this._n = value
  }
})

console.log(`需求二:${data2.n}`)
data2.n = -1
console.log(`需求二:${data2.n} 设置为 -1 失败`)
data2.n = 1
console.log(`需求二:${data2.n} 设置为 1 成功`)

// 抬杠:那如果对方直接使用 data2._n 呢?
// 算你狠

// 需求三:使用代理

let data3 = proxy({ data:{n:0} }) // 括号里是匿名对象,无法访问

function proxy({data}/* 解构赋值,别TM老问 */){
  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 就是 obj
console.log(`需求三:${data3.n}`)
data3.n = -1
console.log(`需求三:${data3.n},设置为 -1 失败`)
data3.n = 1
console.log(`需求三:${data3.n},设置为 1 成功`)

// 杠精你还有话说吗?
// 杠精说有!你看下面代码
// 需求四

let myData = {n:0}
let data4 = proxy({ data:myData }) // 括号里是匿名对象,无法访问

// data3 就是 obj
console.log(`杠精:${data4.n}`)
myData.n = -1
console.log(`杠精:${data4.n},设置为 -1 失败了吗!?`)

// 我现在改 myData,是不是还能改?!你奈我何
// 艹,算你狠

// 需求五:就算用户擅自修改 myData,也要拦截他

let myData5 = {n:0}
let data5 = proxy2({ data:myData5 }) // 括号里是匿名对象,无法访问
// vm = new Vue({data: {...}}) // 是不是很像

function proxy2({data}/* 解构赋值 */){
  // 这里的 'n' 写死了,理论上应该遍历 data 的所有 key,这里做了简化
  let value = data.n
  Object.defineProperty(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 
  // 这段代码为 data 添加了代理功能,obj 就是代理
}

// data3 就是 obj
console.log(`需求五:${data5.n}`)
myData5.n = -1
console.log(`需求五:${data5.n},设置为 -1 失败了`)
myData5.n = 1
console.log(`需求五:${data5.n},设置为 1 成功了`)


// 这代码看着眼熟吗?
// let data5 = proxy2({ data:myData5 }) 
// let vm = new Vue({data: myData})

// 现在我们可以说说 new Vue 做了什么了

所以我们现在知道了 Vue 在初始化的时候做了两件事情:

  1. 让 vm 成为 data 的代理
  2. 对 data 的所有属性进行监听

至此,data 的任何改变 Vue 都可以响应了,而 Vue 帮我们在响应式数据改变的时候重新渲染UI,也就是执行 render 的过程。

Vue 监听 data 的一些问题

如果不声明就使用,Vue 会在控制台给出一个警告

new Vue({
  data: {},
  template: `
    <div>{{n}}</div>
  `
}).$mount("#app");

但是如果写成这样,就不会提示警告,可以看出 Vue 只会检查第一层属性

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

而为了解决新增属性不会监听的问题,Vue 又提供 Vue.set 方法来添加属性监听

Vue.set(this.obj, 'b', 1)
// or this.$set

Vue 3 对响应式原理的实现

因为 Vue 2 不能监测到对象属性的添加删除,所以 Vue 3 的响应式部分重新基于 Proxy 的 observer 实现,它可以提供覆盖语言 (JavaScript) 全范围的响应式能力,消除了当前 Vue 2 基于 Object.defineProperty 所存在的一些局限,这些局限包括:

  1. 对属性的添加、删除动作的监测;
  2. 对数组基于下标的修改、对 .length 修改的监测;
  3. 对 Map、Set、WeakMap 和 WeakSet 的支持;

Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

Vue 3 如何建立响应式

Vue 3 建立响应式的方法有两种:

  • 第一个就是运用 Composition API 中的 reactive 直接构建响应式
  • 第二个就是用传统的 options: {data: return {}} 形式

reactive