说实话,我入Vue坑的时候已经是Vue2.6了,当时觉得Object.defineProperty这个API挺神奇的,靠着getter和setter把data里的每个属性都变成可观察的。虽然网上一堆人骂这玩意儿对数组不友好,对新增属性也不感冒,但我真实项目里其实很少踩坑,因为你必须提前把data写全,否则后面加属性不响应——这反倒逼着我养成了好习惯。直到有一次我在一个表格组件里对数组做下标修改,死活不更新视图,气得我直接用了Vue.set,然后就过去了。后来还有一次,用for...in遍历对象,发现新增的属性在遍历时顺序不是插入顺序,跟Vue2的响应式实现内部枚举有关,当时查了好久,最后知道了也就那样吧。所以我一直觉得Vue2的响应式虽然笨笨的,但坑是死的,记住了就没事了。
后来Vue3出来,一通Proxypiapia打脸旧方案,号称能监听新增属性、删除属性、数组下标……听起来完美极了。我一度很兴奋,赶紧拿个新项目试了试。刚开始用reactive定义个对象,确实爽,直接obj.newProp = 1就响应了,我甚至揉了揉眼睛确认没错。但没过多久我就发现,reactive出来的对象在解构之后就“失灵”了,必须老老实实用toRefs或者干脆用ref。我就在想,怎么反而更复杂了?以前Vue2只要考虑this.xxx就行,现在得区分ref和reactive两种写法,一不小心就忘记.value或者多写了一个.value。有一次我写了个嵌套两层的数据,因为ref的自动解包问题,死活取不到值,打印出来是个Proxy,一层层看才看到target,那一刻我差点把键盘抠了。这件事让我意识到:Proxy虽然更强大,但Vue3的公开API把它封装得不够顺手,以前一套this走天下,现在得时刻惦记着响应式对象到底是个ref还是个reactive,甚至还有shallowReactive、shallowRef这种边角料,简直是给菜鸟挖坑。
再往深了说,Vue3把响应式核心拆成模块,看起来高大上,但是项目里我为了调一个响应式的bug,得去翻源码里的effect、track、trigger,那些Link对象、堆栈逻辑,看得我一个头两个大。而在Vue2时代,我只要知道Object.defineProperty的getter和setter里调用了Dep和Watcher就能把整个流程串起来,源代码简单直白。我不反对框架做优化,但Vue3为了支持各种奇奇怪怪的场景(比如自定义effect、increment、pause等),把响应式的抽象层次整得特别深。有一次我想在组件外共享一个响应式状态,用了Vue3的effectScope,结果因为顺序问题导致副作用函数没有触发,调试了一下午,最后发现是scope没有调用run。这种细节在文档里也就一句话,但实际踩起来真的想骂人。我觉得Vue3的响应式就像一把瑞士军刀,功能全,但每个工具只用一次的话,你就是会搞混哪个是开瓶器哪个是改锥。
我写下这些不是想说Vue3不行,而是觉得每次框架大版本升级,很多人盲目追新,却忽略了“新”并不一定适合你的项目规模。我自己在Vue2写的那个老系统,直到现在还跑得好好的,没出过大乱子。而Vue3的那个新项目,最后我放弃了reactive,全部改用ref,才勉强让自己脑子清醒点。咱就是说,Vue3的Proxy技术确实比defineProperty先进,但由此带来的API分裂和心智负担,是不是值得就那么个“新增属性响应”的优势去换?至少对我来说,不值。也许以后Vue4能把reactive和ref统一了吧,但至少现在,我宁愿继续忍受Vue2那些已经被我背下来的坑,也不愿意去记ref和reactive之间那些微妙区别。反正代码写出来能跑才是最重要的,框架再新,最后还是要看人怎么用它。