从 Vue2 到 Vue3(一):响应式
近来终于有了实战 Vue3 的机会,我当然不能放过。恰好 UED 提出使用新的依赖于 Vue3 的组件库,一拍即合,开搞。
一开始,确实碰到了许多深坑,有时莫名其妙地报红,有时又莫名其妙地变好了,有时纠结在如何组织代码架构,有时又发现想了很多到头来既不简洁也不完备。原来我一心憧憬的 TypeScript 也就那样,它并不如期待的像 Java 那样可以很好的发挥强类型的优势,而且有时不能像 Java 那样,只考虑架构的易用,还需要考虑性能问题
比如,Java 传递数据时,可以随意重新新建实例承接数据,而运行在性能堪忧的浏览器端的 JavaScript,则没办法这么潇洒的处事,具体到这次的项目中,由于涉及 AI,后端的语言无疑首选 Python,Python 的变量风格是下划线,如user_data
,而我当时已经写了多半的 TypeScript 代码,内部组件之间传递的数据,均仍然遵从原有的驼峰命名法,如userData
,如果现在全部改变风格,时间和可能出现的报错都是棘手的问题,而如果全部使用一个工具方法,批量地将下划线格式的变量转换成驼峰命名,则会有两个问题,其一无意义的数据转换拖慢了性能,其二需要另外定义一套类型以适配转换后的对象。最终采取折中方案,内部数据仍然使用驼峰命名,与接口交互的数据对象则使用下划线命名法直接承接。下次遇到 Python 写的后端,八成要预先想好代码格式了,省得这样纠结一下
以上是踩的数个坑中的简单代表,实际还有很多让我纠结的点,这是第一个我写的正式的 Vue3 项目,总想尽可能设计一个比较完善的初始架构,这样可以大幅放缓第一次重构的到来
话不多说,本文不在于纠结上述的坑,而是探讨从 Vue2 到 Vue3 需要经历的原理和代码写法的转变
从 Vue2 到 Vue3(一):响应式
其实我本不想探讨响应式原理,因为这个好像大家都有所了解,但是这里还是多说一嘴
在 Vue2 中,如大家所知,使用Object.defineProperty()
方法劫持数据的读写,从而让 Vue 可以追踪依赖,在 property 被访问和修改时通知变更[1]
Vue 2 使用 getter / setters 完全是出于支持旧版本浏览器的限制。而在 Vue 3 中则使用了 Proxy 来创建响应式对象,仅将 getter / setter 用于 ref。[2]
那么什么是 Proxy?Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)[3],语法如下
1 | const p = new Proxy(target, handler) |
其中handler
为包含捕捉器的占位符对象,包含get
和set
等方法。简而言之与Object.defineProperty()
相似的作用,但不会改变对象。需要说明的是,虽然使用了 Proxy 之后的对象表现得很像原始的对象,但是通过===
还是可以看出区别
下面的伪代码说明了 Vue3 响应式的两大核心 API:
1 | function reactive(obj) { |
可以看出 reactive 使用了 Proxy,而 ref 并没有直接使用 Proxy,而是通过 value 包裹了一层传入的对象,类似 Proxy,但是无法像操作 Proxy 那样无感操作包裹的对象
track
方法收集了依赖,trigger
方法触发了更新方法,代码如下:
1 | let activeEffect |
这里的 activeEffect 是一个全局的值,在尝试触发更新时会赋值为当前的副作用方法,这个稍后再说。getSubscribersForProperty
方法返回了一个存储在全局的WeakMap<target, Map<key, Set<effect>>>
副作用集合映射,可以预见伪代码如下:
1 | let allEffects = new WeakMap(); |
现在假设有这样一种情况:
1 | const obj1 = reactive({k1: 'k1', k2: 'k2'}) |
预期当 obj1 的 k1 或者 k2 发生变化时,update
方法被自动调用,于是 obj2.foo 更新。这需要一个whenDepsChange
方法:
1 | function whenDepsChange(update) { |
可以发现当调用whenDepsChange
方法时,全局的 activeEffect 被赋值,update
方法被触发,此时 obj1.k1 和 obj2.k2 的track
方法均被触发,将当前的effect
方法作为副作用方法记录在各自的副作用集合中,这就算完成了依赖收集
然后当 obj1.k1 或者 obj1.k2 被改变时,它们各自的trigger
方法将各自的副作用方法取出并执行,此时可以预见,上述代码中的effect
方法会再被触发,于是update
方法再被触发,obj2.foo 被自动改变,完成了一次“响应式”
根据 Vue 官网说明,这里建立了一个线上示例:Vue3 响应式 demo
reactive 的局限性
reactive() API 有一些局限性:[4]
- 有限的值类型:它只能用于对象类型 (对象、数组和如 Map、Set 这样的集合类型)。它不能持有如
string
、number
或boolean
这样的原始类型。 - 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失
- 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接
由于这些限制,我们建议使用 ref() 作为声明响应式状态的主要 API。当被 ref 的值是一个对象时,ref() 也会在内部调用 reactive 包装为响应式对象
在使用 ref() 时,需要时刻注意访问或赋值其内部值需要操作value
属性,如果项目使用了 TypeScript,一般会有类型校验提醒