likes
comments
collection
share

Vue 3.2 源码系列:02-面试必会的《响应式设计原则》

作者站长头像
站长
· 阅读数 15

博客代码已上传至github

点击这里 即可访问

另提供:完整代码(ts+rollup)和视频教程

Vue 3.2 源码系列:02-面试必会的《响应式设计原则》

回顾

上一篇文章中咱们一起了解了 vue 框架设计理念,根据上一篇博客咱们知道了:

  1. 命令式
  2. 声明式
  3. 运行时
  4. 编译器
  5. 副作用

对应的概念,不熟悉的小伙伴们,可以点击 这里 进行查看。

正文开始

本篇博客要说明的问题

在咱们日常的工作,特别是 面试 中,面试官经常会问到 vue 响应式设计 相关的问题,在前端越来越卷的今天,如果你连 vue 响应式设计 都回答不出,那么这个工作基本上就与你无缘了。

所以说,本篇博客的目的就是为了给大家说清楚、捋顺溜 vue 响应式设计原则,以帮助看过本篇博客的小伙伴顺利通过 vue 响应式设计 相关的问题面试。

全篇文章大约 3000 字,我们会从:

  1. JS程序性 开始
  2. 讲到 vue 2 响应式核心
  3. 分析 vue2 响应性问题
  4. 引出 vue3 响应性核心

按照以上的顺序,进行讲解。

那么废话不多说,本篇文章内容正式开始~~

JS 的程序性

从我们学习 JS 的第一天就知道,JS 是图灵完备的编程语言。所以JS 将具备完善的程序性。

那么什么是 程序性 呢?下面咱们通过一段代码来进行明确:

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

在上面的代码中:

  1. 首先:咱们定义了一个商品对象 product,它包含price 价格quantity 数量 两个属性
  2. 然后:咱们定义了total 总价格,总价格的计算等于 商品的单价 * 商品的数量
  3. 此时:打印total 应该为 20 元
  4. 然后:我们尝试修改quantity 商品数量,把它的值修改为 5
  5. 最后:再对total 进行打印,那么问大家:此时 total 的值是什么呢?

相信看到这里,大家不免会想:“你这是把我们当小白了吧,这谁不会啊!肯定还是 20 啊!”

恭喜大家!答对了! 第二次的打印结果依然为 20。但是!,大家有没有思考过一个问题,那就是:商品数量发生变化之后,total 真的应该还是 20 吗?

此时你有没有冒出来一个想法:如果商品数量发生变化之后,总价格 total 可以自动重新计算,那就太好了!

但是 js 本身具备 程序性,所谓程序性指的就是:一套固定的,不会发生变化的执行流程

在这样的一个程序性之下,是 不可能 重新发生计算的。

那么如果我们想要拿到这个 50(正确的值) 就必须要让你的程序变得更加的 “聪明”,也就是使其具备 响应性

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

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

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

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

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

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

  // 定义一个商品对象,包含价格和数量
  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

在上面的代码中:

  1. 首先:你封装了一个函数 effect,这个函数可以计算总价格
  2. 然后:你分别在每次打印 total 前,执行 effect 方法,得到最新的总价格

依靠这样的代码,你成功的在第二次打印时得到了我们期望的结果:数据变化了,运算也重新执行了。

太 Beautiful 了~~

但是很快你就发现了一个问题,那就是:必须主动的在数量发生变化之后,重新主动执行 effect ,这样才可以得到想要的结果。

那么这样未免太麻烦了。有什么好的办法吗?

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

你通过主动调用 effect 方法的形式,成功得到了正确的结果。但是你也发现了这样做的一些问题。

所以你开始想:如果不需要主动调用 effect 方法,那就太好了!

你冥思苦想之后,发现:想要达到你的需求,那么可以借助 Object.defineProperty 进行实现。

vue2Object.defineProperty 作为响应性的核心 API ,该 API 可以监听:指定对象的指定属性的 gettersetter

那么接下来咱们就可以借助该 API,让我们之前的程序进行 自动计算,该 API 接收三个参数:

  1. 指定对象
  2. 指定属性
  3. 属性描述符对象

得出代码如下:

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

  // 第一次打印
  effect();
  console.log(`总价格:${total}`); // 总价格:20

  // 监听 product 的 quantity 的 setter
  Object.defineProperty(product, 'quantity', {
    // 监听 product.quantity = xx 的行为,在触发该行为时重新执行 effect
    set(newVal) {
      // 注意:这里不可以是 product.quantity = newVal,因为这样会重复触发 set 行为
      quantity = newVal
      // 重新触发 effect
      effect()
    },
    // 监听 product.quantity,在触发该行为时,以 quantity 变量的值作为 product.quantity 的属性值
    get(val) {
      return quantity
    }
  });

在以上代码中:

  1. 首先:把product 进行了拆分,主要是把quantity 拆分了出来,这样做的目的是为了:setter 和 getter 行为的重复触发
  2. 然后:在第一次打印时,同样触发了 effect 方法,得到第一次打印的总价格
  3. 重点:利用Object.defineProperty方法,监听了productquantity 属性的gettersetter 行为。 当触发set 方法时,会调用 effect() ,从而达到quantity被赋值,重新计算的目的。

现在,你已经成功了利用 Object.defineProperty 方法,完成了基本的响应性。但是这种方法依然不是太好,为什么呢?

Object.defineProperty 在设计层的缺陷

Object.defineProperty 存在缺陷:它只能监听 指定对象的、指定属性的 getter 和 setter

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

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

由于 JavaScript 的限制,Vue 不能检测数组和对象的变化 这是什么意思呢?

咱们通过一段基本的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>

在上面的代码,咱们分别通过:

  1. 为对象增加属性
  2. 为数组增加属性

这么两个例子,来验证了 新增属性 下的响应性,经过验证可以发现:

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

那么为什么会这样呢?

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

我们知道:

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

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

但是 由于 JavaScript 的限制,我们没有办法监听到 指定对象新增了一个属性,所以新增的属性就没有办法通过 Object.defineProperty 来监听 gettersetter,所以 新增的属性将失去响应性。(PS:那么如果想要增加具备响应性的新属性,那么可以通过 Vue.set 方法实现)

那么此时,你已经明确了 Object.defineProperty 所存在的问题,也知道通过 Object.defineProperty 实现响应性可能会存在缺陷,那么怎么办呢?

vue3的响应性核心 API:proxy

Object.defineProperty 的缺陷让你寝食难安,直到你发现了 Proxy ~~~~

当你知道了 Object.defineProperty 的缺陷之后,你迫切的期望想要修正它。这时你注意到了一个新的APIProxy

proxy 顾名思义就是 代理 的意思。我们来看如下代码:

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

  // new Proxy 接收两个参数(被代理对象,handler 对象)。
  // 生成 proxy 代理对象实例,该实例拥有《被代理对象的所有属性》 ,并且可以被监听 getter 和 setter
  // 此时:product 被称为《被代理对象》,proxyProduct 被称为《代理对象》
  const proxyProduct = new Proxy(product, {
    // 监听 proxyProduct 的 set 方法,在 proxyProduct.xx = xx 时,被触发
    // 接收四个参数:被代理对象 tager,指定的属性名 key,新值 newVal,最初被调用的对象 receiver
    // 返回值为一个 boolean 类型,true 表示属性设置成功
    set(target, key, newVal, receiver) {
      // 为 target 附新值
      target[key] = newVal
      // 触发 effect 重新计算
      effect()
      return true
    },
    // 监听 proxyProduct 的 get 方法,在 proxyProduct.xx 时,被触发
    // 接收三个参数:被代理对象 tager,指定的属性名 key,最初被调用的对象 receiver
    // 返回值为 proxyProduct.xx 的结果
    get(target, key, receiver) {
      return target[key]
    }
  })

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

  // 第一次打印
  effect();
  console.log(`总价格:${total}`); // 总价格:20

在以上代码中,我们可以发现 ProxyObject.defineProperty 存在着非常大的区别,那就是:

  1. proxy

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

    1. Object.defineProperty指定对象的指定属性 设置 属性描述符
    2. 当想要修改对象的指定属性时,可以使用原对象进行修改。
    3. 通过属性描述符,只有 被监听 的指定属性,才可以触发 gettersetter

现在你成功的利用proxy解决了Object.defineProperty所带来的的缺陷,真是好极了~~

总结

在本篇博客成,咱们按照:

  1. JS程序性
  2. vue 2 响应式核心
  3. vue2 响应性问题
  4. vue3 响应性核心

的顺序对 vue 响应性 进行了分析,期望大家可以在看完本篇博客的内容之后,达到文章开头我们所提到的目的:顺利通过 vue 响应式设计 相关的问题面试

谢谢大家阅读~~~