vue响应系统的核心设计原则

60 阅读2分钟

01.JS 的程序性

想要了解响应性,那么首先我们先了解什么叫做:JS 的程序性

举个例子

<script>
  // 定义一个商品对象,包含价格和数量
  let product = {
    price: 10,
    quantity: 2
  }
  // 总价格
  let total = product.price * product.quantity;
  // 第一次打印
  console.log(`总价格:${total}`); 
  // 修改了商品的数量
  product.quantity = 5;
  // 第二次打印
  console.log(`总价格:${total}`); 
</script>

运行结果两次都是20 其实我们希望第二次结果为50

02.让你的程序变得更加“聪明”?

你为了让你的程序变得更加 "聪明" , 所以你开始想:”如果数据变化了,重新执行运算就好了“。

那么怎么去做呢?你进行了一个这样的初步设想:

1.创建一个函数 effect,在其内部封装 计算总价格的表达式 2.在第一次打印总价格之前,执行 effect 方法 3.在第二次打印总价格之前,执行 effect 方法

那么这样我们是不是就可以在第二次打印时,得到我们想要的 50 了呢?

所以据此,你得到了如下的代码:

<script>
  // 定义一个商品对象,包含价格和数量
  let product = {
    price: 10,
    quantity: 2
  }
  // 总价格
  let total = 0;
  // 计算总价格的匿名函数
+  let effect = () => {
+    total = product.price * product.quantity;
+  };
  // 第一次打印
+  effect();
  console.log(`总价格:${total}`); // 总价格:20
  // 修改了商品的数量
  product.quantity = 5;
  // 第二次打印
+  effect();
  console.log(`总价格:${total}`); // 总价格:50
</script>

03.vue 2 的响应性核心 API:Object.defineProperty

vue2 以Object.defineProperty作为响应性的核心 API ,该 API 可以监听:指定对象的指定属性的 getter setter行为

API 接收三个参数:指定对象、指定属性、属性描述符对象

举个例子

<script>
  // 定义一个商品对象,包含价格和数量
  let quantity = 2
  let product = {
    price: 10,
    quantity: quantity
  }
  
  // 总价格
  let total = 0;
  // 计算总价格的匿名函数
  let effect = () => {
    total = product.price * product.quantity;
    console.log(`总价格:${total}`); 
  };

  // 第一次打印
  effect();

  // 监听 product 的 quantity 的 setter
  Object.defineProperty(product, 'quantity', {
    // 监听 product.quantity = xx 的行为,在触发该行为时重新执行 effect
    set(newVal) {
      quantity = newVal
      // 重新触发 effect
      effect()
    },
    // 监听 product.quantity,在触发该行为时,以 quantity 变量的值作为 product.quantity 的属性值
    get() {
      return quantity
    }
  });
  
 product.quantity = 5;
</script>

运行结果:

总价格:20

总价格:50

04.Object.defineProperty 在设计层的缺陷

在 vue 官网中存在这样的一段描述 :

由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。

举个例子

<template>
	<div id="app">
		<ul>
			<li v-for="(val, key, index) in obj" :key="index">
				{{ key }} - {{ val }}
			</li>
		</ul>
		<button @click="addObjKey">为对象增加属性</button>
		<hr />
		<ul>
			<li v-for="(item, index) in arr" :key="index">
				{{ item }}
			</li>
		</ul>
		<button @click="addArrItem">为数组添加元素</button>
	</div>
</template>

<script>
export default {
	name: 'App',
	data() {
		return {
			obj: {
				name: '张三',
				age: 30
			},
			arr: ['张三', '李四']
		}
	},
	methods: {
		addObjKey() {
			this.obj.gender = '男'
			console.log(this.obj) // 通过打印可以发现,obj 中存在 gender 属性,但是视图中并没有体现
		},
		addArrItem() {
			this.arr[2] = '王五'
			console.log(this.arr) // 通过打印可以发现,arr 中存在 王五,但是视图中并没有体现
		}
	}
}
</script>

在上面的例子中,我们呈现了 vue2 中响应性的限制:

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

那么为什么会这样呢?

想要搞明白这个原因,那就需要明白官网所说的 由于 JavaScript 的限制 指的是什么意思。

我们知道

  1. vue 2 是以 Object.defineProperty  作为核心 API 实现的响应性
  2. Object.defineProperty 只可以监听 指定对象的指定属性的 getter 和 setter
  3. 被监听了 gettersetter 的属性,就被叫做 该属性具备了响应性

那么这就意味着:我们 必须要知道指定对象中存在该属性,才可以为该属性指定响应性。

但是 由于 JavaScript 的限制,我们没有办法监听到 指定对象新增了一个属性,所以新增的属性就没有办法通过 Object.defineProperty 来监听 gettersetter,所以 新增的属性将失去响应性

05.vue3的响应性核心 API:proxy

因为 Object.defineProperty 存在的问题,所以 vue3 中修改了这个核心 API,改为使用Proxy代理进行实现

举个例子

<script>
  // 定义一个商品对象,包含价格和数量
  let product = {
    price: 10,
    quantity: 2
  }

  // new Proxy 接收两个参数(被代理对象,handler 对象)。
  const proxyProduct = new Proxy(product, {
    set(target, key, newVal, receiver) {
      // 为 target 附新值
      target[key] = newVal
      // 触发 effect 重新计算
      effect()
      return true
    },
    get(target, key, receiver) {
      return target[key]
    }
  })

  // 总价格
  let total = 0;
  // 计算总价格的匿名函数
  let effect = () => {
    total = proxyProduct.price * proxyProduct.quantity;
    console.log(`总价格:${total}`); 
  };

  // 第一次打印
  effect();

  proxyProduct.quantity = 5;

</script>

运行结果:

总价格:20

总价格:50