🔥响应性的核心设计 | 理解为何弃🗑️Vue2之响应性

前言

在本文,我们能够学习到响应系统的核心设计原则,一步步地学习响应性:初步的响应性理解->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中响应性的限制

  1. 当为 对象 新增一个没有在data中声明的属性时,新增的属性不是响应性
  2. 当为 数组 通过下标的形式新增一个元素时,新增的元素不是响应性

总结:Object.defineProperty()用于监听指定对象上指定属性getter行为和setter行为,那么这就意味着:我们必须要知道指定对象中存在该属性,才可以为该属性指定响应性。但是由于javaScript的限制,我们无法监听到 为某一个对象新增了某一个属性的行为 ,那么新增的这个属性就无法通过Object.defineProperty()来监听gettersetter,所以新增的属性不具备响应性数据

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>

一定要使用代理对象,而不是被代理对象(只有代理对象才会触发gettersetter

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:

  1. Proxy将代理一个对象(被代理对象),得到一个新的对象(代理对象),同时拥有被代理对象中所有的属性。
  2. 当想要修改对象的指定属性时,我们应该使用 代理对象 进行修改
  3. 代理对象 的任何一个属性都可以触发 handler 的 getter 和 setter

Object.defineProperty():

  1. Object.defineProperty为 指定对象的指定属性 设置 属性描述符
  2. 当想要修改对象的指定属性时,可以使用原对象进行修改
  3. 通过属性描述符,只有 **被监听 **的指定属性,才可以触发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.fullNamefullName中又触发了this.lastName+ this.firstName ,那么问:getter应该被触发几次?

此时应该为触发3次getter!但实际只触发了1次!为什么?

因为this.lastName+ this.firstName中的 **this**** 为 **p1** ,而非 **proxy**!**所以不会再次触发getter。

所以,如果我们想要"安全"的使用Proxy,还需要配合Reflect一起才可以,因为一旦我们在被代理对象的内部通过this触发gettersetter时,也需要被监听到。

那么这时候就可以用到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>

总结:

当我们期望监听代理对象的gettersetter时,不应该使用target[key] ,因为它在某些时
刻(比如fullName)下是不可靠的 。而应该使用Reflect ,借助它的getset方法,使用receiver(proxy实例) 作为this,已达到期望的结果(触发三次getter)。

相关推荐
GIS程序媛—椰子18 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
ZL不懂前端27 分钟前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x30 分钟前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
我血条子呢1 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落1 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt
麦麦大数据1 小时前
基于vue+neo4j 的中药方剂知识图谱可视化系统
vue.js·知识图谱·neo4j
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea
理想不理想v2 小时前
vue经典前端面试题
前端·javascript·vue.js