likes
comments
collection
share

vue 3 源码设计方案:从源码设计角度看 reactivity 响应性变更

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

让更多的人以更简单的方式学习 vue 3 源码

《vue 3 源码设计方案》系列文章,关于本系列的详细描述,可以点击 这里 ,进行查看。

我们将尝试公开一个 vue 3 的源码学习库 vue-next-mini,点击这里直达。该库可以帮助你快速的学习和掌握 vue 3 源代码的内容,不妨看看~~~~


本节博客,主要讲解 reactivity 响应性,以 vue 2 的响应性核心 API Object.defineProperty 作为入口,分析其使用方案,以及缺陷。从而引出 vue 3 的响应性核心 API Proxy

那么下面我们开始~~~


vue 3 源码设计方案:从源码设计角度看 reactivity 响应性变更

响应式数据

vue 的项目开发中,我们一直在接触 响应性 数据,我们知道 vue 根据使用方式的不同,生成响应性数据有以下几种方式:

1. options API

export default {
  data () {
    return {
      msg: '你好,世界'
    }
  }
}

2. composition API

<script setup>
  import { reactive, ref } from 'vue'
  const msg = reactive({
    msg: '你好,世界'
  })
  
  const msgRef = ref('你好,世界')
</script>

那么大家有没有思考过一个问题,为什么这些数据被叫做响应性数据?什么是非响应性数据呢?

根据我们之前的学习经验可知,所为响应性数据指的就是:数据的改变会引起视图的变化,此类数据就被称作响应性数据。同理,数据的改变不会引起视图变化的数据,就被叫做非响应性数据。

属性标志和属性描述符

我们知道,对象可以存储属性

到目前为止,属性对我们来说只是一个简单的“键值”对。但对象属性实际上是更灵活且更强大的东西。

大家应该还记得,我们之前提到过一个 API Object.defineProperty() ,它的第三个参数为 descriptor 表示 <要定义或修改的属性描述符>。

对象属性的描述符有很多,我们可以通过 Object.getOwnPropertyDescriptor() 来获取它。

当我们用“常用的方式”创建一个属性时,它们都为 true。但我们也可以随时更改它们。

查看下面的例子:

<script>
  const user = {
    name: '张三'
  }
  const descriptor = Object.getOwnPropertyDescriptor(user, 'name')
  console.log(JSON.stringify(descriptor)); // {"value":"张三","writable":true,"enumerable":true,"configurable":true}
</script>

以上属性描述符所代表的意思分别为:

  • value — 当前属性的具体值
  • writable — 如果为 true,则值可以被修改,否则它是只可读的。
  • enumerable — 如果为 true,则会被在循环中列出,否则不会被列出。
  • configurable — 如果为 true该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。

我们可以利用 Object.defineProperty() 修改该 属性 的属性描述符:

Object.defineProperty(user, 'name', {
  // name 属性不可被修改,变为只读的
  writable: false,
  // name 属性不可被枚举
  enumerable: true,
  // 不可再通过 Object.defineProperty 修改属性描述符,也不可以删除该属性
  configurable: false
})

到目前为止,我们知道可以通过 Object.defineProperty 修改一个对象的属性描述符,每个属性描述符都对应不同的作用。

那么对于属性的描述符而言,除了以上列举出来的 4 个之外,还有另外两个非常关键的属性描述符 getset

那么这两个属性描述符的作用是什么呢?我们继续来看。

gettersetter 的概念

到目前为止,我们了解了什么是响应性数据,什么是属性描述符。

并且我们知道在属性描述符中有两个非常重要的描述符:getset。它们是两个函数,也被叫做 访问器属性(accessor property) 。它们本质上是用于获取和设置值的函数。

这两个访问器属性是把非响应性数据变为响应性数据的关键。

那么具体怎么做呢?我们继续往下看。

对于任何 对象的属性 而言,都存在两个行为:

  1. getter 行为
  2. setter 行为

getter 行为

比如我们可以通过 obj.name 的形式,来访问 name 属性。那么此时 obj.name 的触发,本质上就是一个 getter 行为

setter 行为

比如我们可以通过 obj.name = '李四' 的形式,来为 name 属性赋值。那么此时 obj.name = xxx 的触发,本质上就是一个 setter 行为


我们可以通过访问器属性来监听对象属性的这两个行为:

<script>
  const cart = {
    price: 10,
    num: 5
  }
​
  Object.defineProperty(cart, 'num', {
    set() {
      console.log('num 属性,触发 setter 行为');
    },
    get() {
      console.log('num 属性,触发 getter 行为');
    }
  })
</script>

此时在浏览器执行 cart.num 或者 cart.num = xxx 的时候,会成功触发对应的 settergetter

那么接下来我们期望把 cart.num 变为一个响应性数据,根据我们之前所学,我们知道:数据变化,会引起视图发生变化的数据, 就是响应性数据。

那么我们应该如何利用这样的特性来实现响应性的功能呢?

大家可以思考下这个问题,然后,我们继续来说。

vue 2 中响应性核心 API - Object.defineProperty() 的作用

vue2 的响应性核心 API,就是通过 Object.defineProperty() 来进行实现的。那么它具体是怎么做的呢?

我们来看如下这段代码:

<body>
  <div id="app"></div>
</body><script>
  // 购物车
  const cart = {
    price: 10,
    num: 5
  }
​
  // 总价格
  let total = 0
​
  // 计算总价格的方法
  const effect = () => {
    total = cart.price * cart.num
    // 把总价格渲染到页面中
    document.querySelector('#app').innerText = `总价格:${total}`
  }
​
  effect()
</script>

在上面这段代码中,我们构建了一个购物车对象,对象内部包含 price 价格num 数量 这两个属性。同时我们声明了一个 total 属性,表示总价格。

利用 effect 函数,来计算总价格。并把总价格渲染到 #appdiv 元素上。

那么如果我们期望 cart.num 变为响应性数据,那么大家可以想象一下,视图应该具备什么样的特性?

是不是只要:cart.num 的值发生变化时,div 中显示的总价格可以同步跟随变化,那么 cart.num 就可以被称作为响应性数据了。

如果想要做到这一点,那么我们就必须要满足两个条件:

  1. 监听到 cart.num 的值发生变化。
  2. 检测到变化后,需要做什么事情。

那么我们首先来看第一个需求:

第一个需求非常简单,想要监听 cart.num 的值发生变化,只需要通过我们之前学到的访问器属性来 监听 setter 行为 即可。

然后是第二个需求:

而第二个需求就比较难以理解了。我们希望数据变化之后,做些什么事情呢?

有些同学肯定会想,当然是触发 effect 函数了!如果你能够想到这里,那么就证明你已经搞明白了响应性的核心方案。

但是 问题在于,我们为什么要触发 effect 呢?触发 effect 的依据是什么?

仔细观察 effect 函数,可知:在 effect 函数中,我们执行了 cart.num,即:触发了 getter 行为。

即:因为该函数内,触发了 getter,所以该函数内将存在依赖数据产生变化的视图,所以我们应该在 setter 行为之后,触发 effect 函数(即:包含 getter 行为的函数)。


那么明确好了以上两点需求之后,接下来进行对应的实现就非常简单了:

<body>
  <div id="app"></div>
</body><script>
  // 购物车
  let num = 5
  const cart = {
    price: 10,
    num
  }
​
 ...
​
  Object.defineProperty(cart, 'num', {
    set(newValue) {
      console.log('num 属性,触发 setter 行为');
      // 注意这里不可以直接使用 cart.num = xxx 进行赋值,因为这样会再次触发 setter ,导致死循环
      num = newValue
      effect()
    },
    get() {
      console.log('num 属性,触发 getter 行为');
      return num
    }
  })
</script>

此时我们可以在控制台中任意修改 cart.num 的值,可以发现视图跟随发生变化。

在上述代码中,我们把 cart.num 变成了一个响应性的数据,变为响应性数据的方式其实非常简单,主要分为两步:

  1. 利用 Object.defineProperty 监听对象属性的 gettersetter 行为
  2. 在触发 setter 行为时,执行触发了 getter 行为的函数

Object.defineProperty() 的缺陷

截止到目前位置,我们学习了利用 Object.defineProperty() 实现响应性的功能。同时我们也知道 vue 2 的响应性核心 API 就是依赖于 Object.defineProperty() 进行实现的。

但是我们也知道一点,那就是 vue 在某些情况下会丢失响应性

由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。

在这段话的背后,其实就是暴露出来了 Object.defineProperty() 的缺陷,那么这具体指的是什么意思呢?本小节就为大家解惑。

我们可以利用 vue-cli 创建一个 vue 2 的项目,在 APP.vue 中写入如下代码:

<template>
  <div id="app">
    <ul>
      <li v-for="(val, key, index) in obj" :key="index">
        {{ key }} --- {{ val }}
      </li>
    </ul>
    <button @click="addObjKey">为对象增加属性</button>
    <div>---------------</div>
    <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)
    },
    addArrItem() {
      // 视图不会发生变化,数据会发生变化
      this.arr[2] = '王五'
      console.log(this.arr)
    }
  }
}
</script>

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

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

那么为什么会这样呢?

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

我们知道

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

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

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

那么如果想要增加具备响应性的新属性,那么可以通过 Vue.set 方法实现

那么此时,我们已经知道了这些 vue2 中的 “缺陷”,那么 vue3 是如何解决这些缺陷的呢?

我们继续来往下看~~~~

下篇博客点击查看