前言
在本文,我们能够学习到响应系统的核心设计原则,一步步地学习响应性:初步的响应性理解->Vue2的响应性->Vue2的响应性缺陷->Vue3的响应性->完善的Vue3响应性。🔥
认识下JS的程序性🤖
先来了解一下 JS的程序性:
"看到下面这段代码,在第二次打印时你真的希望它还是输出20吗?"
            
            
              html
              
              
            
          
          <script>
  // 定义一个商品对象
  let product = {
    price: 10,
    quantity: 2
  }
  // 总价格
  let total = product.price * product.quantity
  console.log(`总价格:${total}`) //20
  // 修改商品的数量
  product.quantity = 5
  console.log(`总价格:${total}`) //20
</script>
        我们希望的是如果商品数量发生变化了,如果总价格能够自己跟随变化 ,这就是我们所期盼的响应性。
但是 js 本身具备程序性,所谓程序性 指的就是:一套固定的,不会发生变化的执行流程 ,在这样的一个程序性之下,我们是不可能 拿到想要的50的。
响应性的一步步实现🐾
初步的响应性理解->Vue2的响应性->Vue2的响应性缺陷->Vue3的响应性->完善的Vue3响应性
初步的响应性理解💬
对于product.quantity而言,第二次打印要想实现响应式的话,必须按照一套逻辑:"首先是修改商品数量触发setter行为,接着手动调用effect(),触发商品数量的getter行为"
然而每次都手动调用effect(),也太麻烦了吧!
            
            
              html
              
              
            
          
          <script>
  // 定义一个商品对象
  let product = {
    price: 10,
    quantity: 2
  }
  // 总价格
  let total = 0
  // 计算总价格
  let effect = () => {
    total = product.price * product.quantity // product.quantity触发getter行为
  }
  effect()
  console.log(`总价格:${total}`) //20
  // 修改商品的数量
  product.quantity = 5 // product.quantity触发setter行为
  effect()
  console.log(`总价格:${total}`) //50
</script>
        Vue2中的响应性
Vue2 使用 Object.defineProperty作为响应性的核心API,Object.defineProperty()用于监听指定对象上指定属性的getter行为和setter行为。
"那么就意味着我们可以不用手动调用effect()了,让api自己帮我们监听并调用。"
            
            
              html
              
              
            
          
          <script>
  // 定义一个商品对象
  let quantity = 2
  let product = {
    price: 10,
    quantity: quantity
  }
  // 总价格
  let total = 0
  // 计算总价格
  let effect = () => {
    total = product.price * product.quantity
  }
  // 第一次打印
  effect()
  console.log(`总价格:${total}`) //20
  Object.defineProperty(product, 'quantity', {
    set(newVal) {
      console.log('setter')
      quantity = newVal
      // 调用effect()
      effect()
    },
    get() {
      console.log('getter')
      return quantity
    }
  })
</script>
        去浏览器中输出试一下:

为什么Vue3放弃了这种方式呢?
这是因为 Object.defineProperty 存在一个致命的缺陷!
vue官网中存在这样的一段描述:
由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。
"这是什么意思呢?"
对象的变化
对于对象而言,例如在你的页面上通过v-for 循环渲染出了{name:'张三',age:30}这个对象的数据,

同时你定义了一个方法,能够为这个obj对象增添一个gender属性,并且打印出obj

在按下按钮后,我们发现虽然输出的obj包含了gender属性,但是页面的视图并没有更新!即这个gender并不是响应性的!也就是说Vue不能检测 对象 的变化 !
数组的变化
对于数组而言,还是同样的例子,你定义了一个方法通过数组下标新增数组元素,并且打印出arr

现象与上面一致,也就是说Vue不能检测 数组 的变化 !
在上面的例子中,我们呈现了vue2中响应性的限制:
- 当为 对象 新增一个没有在data中声明的属性时,新增的属性不是响应性的
 - 当为 数组 通过下标的形式新增一个元素时,新增的元素不是响应性的
 
总结:Object.defineProperty()用于监听指定对象上指定属性 的getter行为和setter行为,那么这就意味着:我们必须要知道指定对象中存在该属性,才可以为该属性指定响应性。但是由于javaScript的限制,我们无法监听到 为某一个对象新增了某一个属性的行为 ,那么新增的这个属性就无法通过Object.defineProperty()来监听getter和setter,所以新增的属性不具备响应性数据
Vue3中的响应性
因为 Object.defineProperty 存在的问题,所以Vue3中修改了这个核心API,改为使用 Proxy 进行实现。
proxy 顾名思义就是代理的意思。我们来看如下代码:
            
            
              html
              
              
            
          
          <script>
  // 定义一个商品对象
  let product = {
    price: 10,
    quantity: 2
  }
  // product:被代理对象
  // proxyProduct:代理对象
  const proxyProduct = new Proxy(product, {
    set(target, key, newVal, receiver) {
      console.log('setter')
    },
    get(target, key, receiver) {
      console.log('getter')
    }
  })
  // 总价格
  let total = 0
  // 计算总价格
  let effect = () => {
    total = proxyProduct.price * proxyProduct.quantity 
  }
  // 第一次打印
  effect()
  console.log(`总价格:${total}`) //20
</script>
        一定要使用代理对象,而不是被代理对象(只有代理对象才会触发getter和setter)
 
Vue3基本的响应性 Proxy
            
            
              html
              
              
            
          
          <script>
  // 定义一个商品对象
  let product = {
    price: 10,
    quantity: 2
  }
  // product:被代理对象
  // proxyProduct:代理对象
  const proxyProduct = new Proxy(product, {
    set(target, key, newVal, receiver) {
      // console.log('setter')
      target[key] = newVal
      // 触发effect()
      effect()
      return true //默认setter行为完成后返回true
    },
    get(target, key, receiver) {
      // console.log('getter')
      return target[key]
    }
  })
  // 总价格
  let total = 0
  // 计算总价格
  let effect = () => {
    total = proxyProduct.price * proxyProduct.quantity //改为代理对象才会触发getter和setter
  }
  // 第一次打印
  effect()
  console.log(`总价格:${total}`) //20
</script>
        
Vue2和Vue3响应性区别
proxy:
- Proxy将代理一个对象(被代理对象),得到一个新的对象(代理对象),同时拥有被代理对象中所有的属性。
 - 当想要修改对象的指定属性时,我们应该使用 代理对象 进行修改
 - 代理对象 的任何一个属性都可以触发 handler 的 getter 和 setter
 
Object.defineProperty():
- Object.defineProperty为 指定对象的指定属性 设置 属性描述符
 - 当想要修改对象的指定属性时,可以使用原对象进行修改
 - 通过属性描述符,只有 **被监听 **的指定属性,才可以触发getter和setter
 
如此看来,由于 proxy 没有指定属性 这个概念,而是使用代理对象 的概念,所以 vue3 将 不会 再存在新增属性时失去响应性的问题!
Vue3完善的响应性 Proxy+Reflect
当我们了解了Proxy之后,那么接下来我们需要了解另外一个Proxy的"伴生对象":Reflect
说起Reflect.get,总感觉他是个多余的Api,就像如下代码:
            
            
              javascript
              
              
            
          
          const obj = {
  name: '张三'
}
console.log(obj.name);//张三
console.log(Reflect.get(obj,'name'));//张三
        但其实Reflect.get重要的地方在于它的第三个参数 Reflect.get(target,propertyKey[,receiver])
官方的介绍为:
如果target对象中指定了getter,receiver则为getter调用时的this值。
            
            
              html
              
              
            
          
          <script>
  const p1 = {
    lastName: '张',
    firstName: '三',
    get fullName() {
      return this.lastName + this.firstName
    }
  }
  const p2 = {
    lastName: '李',
    firstName: '四',
    get fullName() {
      return this.lastName + this.firstName
    }
  }
  console.log(p1.fullName) //张三
  console.log(Reflect.get(p1, 'fullName')) //张三
  console.log(Reflect.get(p1, 'fullName', p2)) //李四
</script>
        此时触发的 fullName 不是p1的而是p2的。
接下来我们看看下面代码的打印情况:
            
            
              javascript
              
              
            
          
          <script>
  const p1 = {
    lastName: '张',
    firstName: '三',
    get fullName() {
      console.log(this)
      return this.lastName + this.firstName
    }
  }
  const proxy = new Proxy(p1, {
    get(target, key, receiver) {
      console.log('getter行为被触发')
      return target[key]
    }
  })
  console.log(proxy.fullName)
</script>
        
这是我们预想要得到的打印结果吗?我们触发了proxy.fullName,fullName中又触发了this.lastName+ this.firstName ,那么问:getter应该被触发几次?
此时应该为触发3次getter!但实际只触发了1次!为什么?
因为this.lastName+ this.firstName中的 **this**** 为 **p1** ,而非 **proxy**!**所以不会再次触发getter。
所以,如果我们想要"安全"的使用Proxy,还需要配合Reflect一起才可以,因为一旦我们在被代理对象的内部通过this触发getter和setter时,也需要被监听到。
那么这时候就可以用到Reflect.get的第三个参数了
            
            
              javascript
              
              
            
          
          <script>
  const p1 = {
    lastName: '张',
    firstName: '三',
    get fullName() {
      console.log(this)
      return this.lastName + this.firstName
    }
  }
  const proxy = new Proxy(p1, {
    get(target, key, receiver) {
      console.log('getter行为被触发')
      // return target[key]
      return Reflect.get(target, key, receiver)
    }
  })
  console.log(proxy.fullName)
</script>
        
总结:
当我们期望监听代理对象的getter和setter时,不应该使用target[key] ,因为它在某些时
刻(比如fullName)下是不可靠的 。而应该使用Reflect ,借助它的get和set方法,使用receiver(proxy实例) 作为this,已达到期望的结果(触发三次getter)。