ToC
响应式例子
在 Vue 中,无论是 2 还是 3,它们使用起来差别都不太大,我们先看一个例子,借助它来分析一下响应式系统:
页面中一共有三个关于对 price
变量的访问 price
price * quantity
totalPriceWithTax
,当我们的 price
发生变化的时候, 页面中的 price
price * quantity
totalPriceWithTax
变量也会随之发生改变, 而这一连锁反应就是响应式系统的优势,即:自动更新依赖项数据。
所以问题是我又是怎么知道变量被依赖了,又应该去更新哪些数据的? 因为这不是 JavaScript 通常的工作方式, 在常规情况下,我们它们会以下面这种方式来工作:
那么接下来我们一步一步尝试解决这个问题。我们需要提出问题,怎样存储 total
的计算方式才能当 price
或 quantity
发生变化的时候,让 total
重新计算一次?
手动收集、更新依赖
我们思考一下,上述语句中,是什么让 total
变量发生了变化?是的,是 total = price * quantity
。那我们只需要在我们想要变化的时候重新执行这一语句即可。我们知道,在编程语言中想要对代码进行一定程度的复用的话,可以利用函数的能力。我们将这一语句保存到一个函数中,将这个函数保存在一个 storage
中,当我们想要再次访问的时候,只需要从 storage
中取回这个函数并执行它即可达到效果,但是我们可能不止保存这一个函数,所以可能还会存在多个不同的 storage
。那么我们按照这个思路对上述代码进行一次改写。
我们达成了我们想要的效果,但我们在开发中会有很多数据有着这种依赖关系,每一个数据都去手动维护它们的状态更新显然效率更低,因为每个属性都需要自己的 deps
,或者说是这个变量自己的 effects
的 Set
集合。那么更好的做法显然是让每个熟悉都拥有一个自己的 effects
。
每个数据与依赖项的对应关系如下:
depsMap
保存了所有属性的依赖变化项,它的类型是一个 Map
,而它可能会有 price
或 quantity
属性,它们的值是一个 Set
类型的 deps
。而 deps
类型中保存着其属性的 effects
。那么我们尝试去实现它:
这是一个比较基础的响应式系统原型, 那如果我们有多个响应式对象数据的话,光是这样做还远远不够。假设我们有 user
product
等其他响应式对象时,我们可能需要做更多工作。那么这时候就需要其他的对象,它的 key
以某种方式引用了我们的响应式对象,例如: product
user
。
多个响应式对象
那么我们根据上面的信息可以得到这些:
我们最终的结果没有任何变化,但是执行的过程变了,这么做会使得它在面对多个响应式数据对象的时候也可以轻松应对。
但如果代码的复杂度进一步提升时,我们想要维护繁多的响应式对象的难度将会进一步提升,那么我们接下来要做的事情则是将响应式对象的依赖收集和响应式触发进一步封装,使其自动化,来达到访问属性的时候自动收集依赖,修改属性值的时候自动执行依赖项以保证数据获取的最新值。
过程自动化
如何去做?我们可以借助 Proxy 和 Reflect 对对象代理的能力实现这个功能,代理整个对象,通过监听其属性的访问和覆写来自动完成收集、触发的操作。我们首要介绍一下为什么要使用 Reflect
,以及使用它有什么好处。简单对比一下三种可以对象属性的方式:
它们都可以达成目标,但是 Reflect
配合 Proxy
的话,有一点是上面两种做不到的,即:保证当对象有继承自其他对象的值或函数时,this 指针能正确的指向使用的对象
。使用方法为:Reflect.get(target, key, receiver)
和 Reflect.set(target, key, value, receiver)
我们先了解一下 Proxy
的工作流程:
在了解基本工作流程后,我们试着用一个例子去理解它:
之后我们尝试去封装它:
回到我们原来的代码中,并应用这个新的函数:
我们将所有需要手动执行的 track
trigger
都封装到了 reactive
函数内部,让程序去帮助我们去做这件事,这会使得我们在读取属性的时候,依赖项的收集和触发将变得简单了起来。这样一来,基本的响应式就完成了。
完整代码
完整代码如下:
以上。