从 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,一般会有类型校验提醒



Mosu is located on the shore of Mosu Lake, facing the vast Chu Sea, backed by the Yihan Mountains. Thousands of miles of Mosu Desert can not erode the Mosu Valley. Thus the Mosu Empire was established.


