Vue 3.0 Ref-sugar 提案真的是自寻死路吗?

Vue 3.0 Ref-sugar 提案真的是自寻死路吗?

Vue 3.0 的两个提案,最近吸引了许多开发者的注意跟讨论。一个是 script-setup 提案,一个是 ref-sugar 提案。

对于 script-setup 提案,大部分开发者持正面态度。

对于 ref-sugar 提案,却有相当数量的开发者表达否定态度。

在 Github 相关 RFC 里[0],以及知乎问答上[1],国内外的开发者都积极发表了意见。其中涌现了一些值得咀嚼的看法和现象。

这篇文章将选取几个有趣的片段,加以解读。

一、Vue 提案实际在做什么?

首先,我们先来看一下,Vue 的那两个提案在做什么。

script-setup 提案,是将 options.setup 提取到 top-level。

将下面的写法:

用下面这种更简洁形式替代:

而 ref-sugar 提案,则是将 ref.value 的写法,做进一步简化。

如上所示,代码行数在这两个提案作用下,变得更少更简洁。

这就是 Vue 新提案实际在做的: 在 Vue SFC 文件里,提供语法糖,优化编程体验。

二、批评者认为 Vue 提案在做什么?

我们摘录一些在讨论中,部分开发者对 Vue 提案所作的解读。

1、
请不要再制造更多的 js 方言了
2、
感觉不太靠谱,跟 js 开发者的直觉不一样,容易陷入混乱。
3、
这是一个极其危险的信号
因为修改js语义意味着与生态脱节,而vue的团队说实话还不能自给自足打完一整个生态。
目前来看label语义的修改并不会过多影响,但是会导致开发者对以后vue的生态兼容性产生疑虑从而丢失开发者。
在web的世界里,与标准脱离的,没有一个有好下场,除非倒逼标准,否则没有漏网之鱼。
4、
vue和react都用过一段时间,vue感觉是经常忘记语法,需要对照文档才知道怎么写,react很少需要查阅文档
5、
等过这一套标准越来越复杂,需要重复理解的东西越来越多,并且它并不能给程序员带来深度上的提升,而只是用于写表面的编程的时候,你会发现,这套框架被淘汰了
6、
很不看好,对于动js的语义形成方言这种解决方案应当慎之又慎,为了解决.value的问题是否值得?
7、
别再使用魔法了☹️
8、
想了一整天都没想明白,尤大大会搞出这样子富有争议的rfc。我总觉得使用装饰器会比这个更容易接受…
9、
个人不是很喜欢,违反直觉,不好用。需要增加用户理解成本

三、小改进 vs 大危机?

光从代码形式上看,Vue Ref-sugar 提案属于一个有效的小改进,用更简短精炼的代码,表达相同的逻辑。

然而从概念上,部分批评者却从中解读出了大危机:Vue 企图挑战标准

批评者认为 Ref-sugar 是一个危险信号,意味着 Vue 即将开启一条背离 Web 标准的路线,意味着技术栈押注到 Vue 不再安全,意味着 Vue 背叛了之前的承诺。

对反标准的行为有巨大的反感态度,其实完全可以被理解。

前端工程师这么多年,深受各大浏览器对 Web 标准兼容性不足所累。但凡某个浏览器跟进标准不积极,相比其它浏览器落后许多,就会被戏谑为:XXX is new IE。

我们希望所有浏览器拥抱 Web 标准,我们希望主流框架不要自己搞一套玩法,我们希望在一个坚实的、统一的底层下,专注于产品实现,去解决更有价值的事情,而不是陷入低级问题的兼容处理。

捍卫标准,在前端领域,有着天然的正义性。

因此,批评者认为,Vue 应当在符合标准的前提下,进行改进。

四、标准之下的解决方案们

在这里,我们挑几个开发者们提出的、符合标准的,他们觉得也能解决问题的候选方案。

4.1、问题不存在或不必解决

部分开发者认为,ref.value 不是什么问题,不必解决。

特别的,如果用了 TypeScript 语言,IDE 和编辑器会智能提示,几乎不会出现漏写 .value 的情况,他们也不觉得有什么心智负担。

问题是,并非所有人都使用 TypeScript 编写 Vue 代码,也并非所有开发者都不觉得 .value 是个问题。

4.2、采用 proposal-refs 提案[2]

有开发者搜索出了 JavaScript 里的一个 proposal-refs,并认为这可能是既符合标准,又能帮助 Vue 解决问题的方案。

let ref count = 0;

如果仔细看 proposal-refs 提案,可能发现,它跟 vue-ref 根本不是一回事儿。vue-ref 是 reactive-ref,而 proposal-refs 提案描述的是传统意义上的 Reference Binding。只是都用了 ref 这个词儿。

更何况,这个 proposal-refs 提案,还是一个几年没有动静、没有进展,离成为真正的标准,遥遥无期。无法解决 Vue 眼前的问题。

4.3、采用 proposal-decorators 提案[3]

有开发者又提出,新的 decorators 提案里,支持 Variables Decorators。

即,可以对变量声明进行装饰,放到 vue-ref 场景里,大概像下面这样。

let @ref count = 0

看起来,非常有吸引力,仿佛问题已得到圆满解决。但是:

第一,它太新了,是最近才提出的一个设想,能不能落地,什么时候能落地,完全不可期待。远水救不了近火。

第二,它跟当前几个 decorators 提案的语义和用法不兼容。一旦 vue 采用新版装饰符提案,很可能意味着,当前被大量使用的、主流风格的 decorators 用法,不能用,有冲突。

第三,它只解决了 vue-ref 的 create 部分的问题,没有解决 access 和 update 部分的问题。

什么意思?

装饰符只是帮我们创建了一个 ref,但访问 ref.value 和更新 ref.value 部分是缺失的。

将 proposal-refs 跟 proposal-decorators 结合起来,倒可以克服上述困难。变成下面这种写法:

let @reactive ref count = 0

顺序很重要,先变成 ref 对象,再添加 reactive 能力;如果是下面这种,恐怕不行:

let ref @reactive count = 0

它变成 ref 了一个 { value: 0 },后续还得带着 .value。

可惜的是,上述设想,是在两个落地概率低迷的提案下,叠加起来更加低迷的情况。很难有所期待。

4.4、变量命名约定方案

有开发者提出,可以约定 ref 变量名,去标记一个变量属于 vue-ref。

或者简化 .value 属性。

尽管没有彻底解决问题,但他们认为这样减轻了问题。

从实践上,这种做法或许可行,但这种解决水平,可能不值得。开发者依然被时刻提醒,所操作的变量在一个 ref 包装中。

4.5、comment-based 方案

有开发者提出,可以通过 comment 注释,去标记一个变量属于 vue-ref。

这种做法不无道理,JSDoc 就用注释的方式,去做函数类型标记和文档描述,并取得了成功。

然而,vue-ref 的问题,还有另一面:访问 ref 对象。我们无法 watch 一个 Primitive value,我们需要 watch 一个 Reactive value。

因此,comment-based 方案,即便放弃了 ref: 标签语法糖的部分,可能也需要保留 Ref-sugar 的另一个特性:$count。

也就是说,对于解决 vue-ref 问题来说,comment-based 自身是一个不完整的处理。也会带来新问题,开发者可能开始频繁忘记加注释标记,需要建立先写注释再写变量声明的新习惯,也是一大挑战。

还有其它一些方案未被提及,它们大多更不能打。

总的来说,这些标准之下的其它选择,都有各种各样的缺陷,并不能充分解决 vue-ref 的问题。

五、标准的构成和演化

回想一下,那些批评 ref-sugar 违背规范和标准的表述。

标准一词,主要起到不容置疑、不容违背、不容商榷的作用,不是一个清晰的面貌。

大家将标准和规范,想象得太完美了。

其实标准和规范,并非铁板一块,并非处处不可妥协。如果我们愿意凑近标准和规范,我们会看到更多细节……

5.1、标准和规范里存在落后于时代的部分

标准和规范,也起到一个记录和向后兼容的作用。特别是像 JavaScript 这类先事实上存在,后面才去撰写标准的事物。

在 ECMAScript 规范里,不乏对 JavaScript 老旧的、落后于时代的、不再被推荐使用的语言特性的整理和描述。如 arguments, with, prototype 等。

因此,即便符合标准和规范,也不一定是好的、正确的、符合时代需求的。

5.2、标准和规范里,不是所有东西都一样重要

Vue Ref-sugar 提案,其实并未修改 JS 语法,只是赋予 labeled statement 新的语义,用以声明一个 vue-ref 对象。因此 Vue 作者尤小右表示,它仍然是合乎语法的 JS 代码。

然后,有开发者从 ECMAScript 规范里[4],找到了这一句:

15.1.1 Static Semantics: Early Errors
It is a Syntax Error if ContainsDuplicateLabels of StatementList with argument is true.

在相同作用域内,出现重名的 labels,在 Static Semantics 层面上,属于 Syntax Error,即语法错误。

部分开发者因此认为,这有力地驳倒了尤小右声称的合法 JS 代码,这坐实了 Vue Ref-sugar 不仅从语义上违背规范,语法上也破产了,属于彻头彻尾地反规范事物,应当放弃。

这个看法,可能也不太能站得住脚。

因为,Static Semantics 也是语义的一种[5],只是它的一些规则,恰好描述的是语法结构。当尤小右说 ref-sugar 依然是合法 JS 代码时,他说的合法,有严格的特指,就是 Parser 层面的。他不曾试图表达过任何 Static Semantics 等其它层面的合法性。

他一直强调的是,Babel、Webpack、TypeScript、ESLint、Prettiter、V8 等工具和运行时层面对 JS 代码解析上的合法性。

我们不能驳倒一个人没发表过的意见。

更重要的是,经过测试和验证,Babel、Webpack、TypeScript、ESLint、Prettiter、V8 等工具,全部没有实现 ContainsDuplicateLabels 的 Static Semantics 特性。

如上,在 chrome 控制台里,运行包含重名 labels 的代码,并无任何问题。

也就是说,事实上,从实践层面,并非标准和规范里所有部分,都有同等的重要性。一些无伤大雅的部分,许多工具和 JS 引擎,并没有实现。

因此,并非标准和规范里的所有部分,都值得我们采取一样强硬的态度。

5.3、标准和规范是演化的、语义是可以调整的

批评者说 ref-sugar 修改了 labeled statement 的语义,属于大逆不道。

然而事实上,在某个上下文里,调整原有语法的语义,是非常自然的、常见的、可行的做法。

JavaScript 语言自己也干这事儿——严格模式。

JS 代码在严格模式和非严格模式下,相同语法,也有不同语义行为。

未声明就做变量赋值,在非严格模式下,相当于给全局变量赋值;但在严格模式下,则抛出错误。

同一段代码,ES3 和 ES5 两个标准产生了不同的语义解读。

标准不是一成不变的,标准也在发展,在演化。

当然,批评者或许继续坚持:ES5 相对 ES3 是新的语言标准,只有新的语言标准可以做出调整,而 Vue 只是 JS 语言里的其中一个框架,它不能这样做。

那么,我们来看下面这段代码:

熟悉 Node.js 的开发者,立刻识别到,上面不就是一段 node.js 的 hello world 示例代码吗?跟我们现在讨论的问题,有什么关系?

从功能角度去理解的话,确实如此。如果我们从 JS 语法和语义角度去看,情况就不同了。

很容易发现,从 JS 代码原始语义来看,上面的代码,应该产生 4 个全局变量:http, hostname, port, server,因为它们在 global scope 里被声明和使用。

然而,事实上它运行在 node.js 里,并不会产生上述全局变量,它们会被包裹在一个函数里执行,它们是一个 commonjs module,而不是朴素的 JS 代码。

也就是说,这段 JS 代码,在 commonjs module 这个上下文里,它的语义行为被改变了,从挂载全局变量,变成 commonjs module 里的局部变量。

node.js 是一个 runtime/platform,它不是 ES5 性质的语言规范,它也在修改 JS 代码的语义/行为。

那么,ref: count = 1 这句 JS 代码,在 vuejs SFC 这个上下文里,它的语义行为也发生一些调整,从性质上,跟 ES5 和 Node.js 上下文里做调整,又有什么本质区别呢?

调整代码的语义行为这事儿,ES5 新语言规范做得,Node.js 新平台做得,Vue.js 新框架就做不得吗?

批评者或许继续缩小它的抨击范围:Vue.js 自然是可以调整一些语义行为,script-setup 提案就调整了 top-level 的输出,将它输出到了 options.setup 里,大部分开发者都表示认可。但不管 ES5、Node.js 还是 script-setup,都是在原有语义下的小调整,要么报错,要么从全局变量变成局部变量。

但是,ref-sugar 是将 labeled statement 赋予了跟它之前语义毫不相干的新语义,这是难以接受的。

5.4、标准和规范往往滞后于实践

假设开发者们,严格地在标准之下行事,技术发展可能陷入迟缓,甚至是停滞。

标准和规范,常常是滞后于实践。先有了实践层面的各色探索,形成了很多形式上不尽相同、但本质上差异不大的多种解决方案后,产生了标准化的需求。

Babel 工具的一个用途就是,先让开发者在实践中,去编写还没完全进入到标准里的新语言特性,根据开发者实际使用的反馈,调整该 proposal 的 stage,最终落地到标准里。

也就是说,作为前端工程师的我们,本来就被允许使用一些非当前稳定规范下的技术特性。

如果大家觉得,Babel 推进 proposal 起码是一条走向规范的路线,而不是像 Vue 的 Ref-sugar 那样,可能走向规范的反面。那么:

TypeScript 语言的存在,它为 JavaScript 插上了 Type-System 的翅膀,而 ECMAScript 规范,对 Typed-JS 并没有任何表示要纳入的意思。

这意味着,使用 TypeScript 也不是一条走向 ECMAScript 规范的路线。甚至 TypeScript 自身,目前为止也没有规范。

那这意味着,前端工程师们,使用 TypeScript 也是大逆不道、违背 ECMAScript 发展路线的事情吗?

标准和规范,不一定提供了我们想要的全部工具,有的它还没提供,但将来会提供,同时也需要我们配合使用不稳定的标准特性;有的它不会提供,我们需要寻求其它途径和工具,去代为满足。

这种超前的实践探索,不仅仅发生在 Babel/TypeScript 等语言层面,在前端框架层面,也存在。

React 的 JSX 语言拓展,就不属于 ECMAScript 规范的范畴。但不妨碍它当前流行于前端开发中,带来巨大价值。

React Team 成员 Sebastian 在 2016 年,曾经提了一个 one-shot Delimited Continuations with Effect Handlers 提案[6],他认为这个特性,对于 UI Framework 很有价值,希望能推动它的进展。但最后,提案不了了之。

React Team 并没有停下来等待标准,而是选择重构 React 底层,采用 Fiber Architecture,由框架接管组件的 call-stacks 管理,通过各种技术手段,模拟 Algebraic-Effects 特性。后续推出了 React-Hooks 新特性,对 Vue 等其它框架的发展,也起到了正面影响。

标准再一次滞后于实践,并将继续滞后。

技术发展,需要超前实践探索,我们既要允许 Node.js 这类新平台,TypeScript 这类新语言,Babel/JSX 这类新工具提供一些新用法,也应允许 React,Vue 等新框架或框架的新版本,做出它们的新探索。

正是这些新探索,帮助规范指明了方向。知道哪些模式,已经被证明是极有价值的。

因此,Vue SFC 里对 labelled statement 语法,做一些有效的、安全的语义挖掘,并没有大家想象得那样反常。

5.5、规范不是唯一权威来源

如果我们将视野继续打开,跳出 JavaScript 层面。我们可以会发现,ECMAScript 规范,只不过是其中一门语言里的规范。

这个世界上,还存在很多语言,它们也有自己的优秀之处。它们值得 JavaScript 学习。事实上,JavaScript 已经向其它语言,学习到很多东西了。

那么,在其它语言里,Vue Ref-sugar 这种性质的做法,也是如此大逆不道的吗?

不是的。

比如,$count 这种约定式变量,在 Swift/Kotlin 等语言里也存在。

如上,Swift 的 computed property 支持 Shorthand Setter Declaration 写法[7],即可以省略 set(value) {},用约定式变量 newValue 访问 setter 参数。

尽管 newValue 只有一个变量,而 Vue Ref-sugar 根据 ref: 的次数,可以创建很多 $ref。但这只是数量上的差别,从性质上,它们是一样的。

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiWayIf #-}
module ShellCheck.Parser (parseScript, runTests) where

如上,Haskell 支持通过 {-# LANGUAGE * #-} 的语法,添加语言特性插件。

甚至,有的支持 Macro 的语言,如 Rust,它们本来就提供了改变语法和语义的标准工具。

如上,rust 的 serde_json 提供了 json! macro,让开发者可以在 rust 代码里编写 json。

如上,rust 的 typed_html 提供了 html! macro,让开发者可以在 rust 代码里,编写 html 标签结构。

如上,rust 的 juniper 提供了 Derive macros,可以标记一个 rust enum 对应了一个 GraphQL Enum。

这种做法,类似于前面提过的 comment-based 方案。但不管是 comment-based 还是 label-based,只是形式不同,重要的是,它们都是通过标记的方式,修改或者增强特定语法的语义。

我们不能因为当前 ECMAScript 没有提供类似 Language Extension 或者 Macro 的功能,就放弃学习在其它语言里,业已被证明可以带来正面价值的做法。

当前主流前端开发基础设施里,其实已经带了多个 Parser 和 Compiler,如前面列举的 Babel、Webpack、TypeScript、ESLint、Prettiter、V8 等,Vue 也有自己的 vue-compiler。

我们的代码,几乎都通过多次编译处理。从工具链的能力上,完全可以提供类似 language extension 和 macro 性质的转换。babel plugin,babel macro, angualr-compiler, vue-compiler 等,已经对一些 template 事物,进行过这类处理。

其实迟早有一天,前端框架会逐渐榨干当前 JavaScript 语言的表达能力,继续深挖,也很难有效解决问题。需要动用 compiler 的处理。这正是 Svelte 在努力的方向,它充分发挥了 compiler 的能动性,不通过 runtime 抽象,通过 compile-time 转换,实现 reactivity 机制。

React 在 runtime 抽象上深耕已久,也实现了 Concurrent Mode 这种复杂的调度处理。但依然在探索通过 compiler 去自动补充 useEffect 依赖数组的可行性。

Angular 的 next-generation compilation and rendering pipeline 技术——Ivy,也是通过编译技术去优化。

早在几年前,社区就喊出了 Compilers are the New Frameworks 的声音。

Vue 不是第一个迈出这一步的。相反,在主流框架里,Vue 可能是最谨慎的那一个,它选择 labeled statement 语法,正是受到 Svelte 里相似用法的启发。已经是在前人探索的成果下,所作的相对成熟的考量。

六、Label-sugar 提案

Vue 的谨慎之处也体现在,其实 Ref-sugar 是更普适的 Label-sugar 的特例。

Vue team 深深地考虑过大家对新语义可能的排斥,决心用最小的、最低成本的牺牲,去解决 vue-ref 的易用性问题。

在此,我们可以做个头脑风暴,看看更普适的 Label-sugar 是怎样的。

比如,提供更多不同作用的 label。

如上,用 expose: 来表达组件对外的输出,用 mounted: 表达 onMounted,用 unmounted: 表达 onUnmounted。

label 可以组合起来,expose: ref: 表达对外输出一个 ref。

再比如,用在 React 组件里:

如上,我们用 state: 表达 useState,用 effect: 表达 useEffect,用 callback: 表达 useCallback。组件代码写起来,更加规整和简洁。

这是一个有趣的设想,我个人并不排斥这种风格。在我看来,它相当于对 commonjs module 这种性质的处理的细化,从 module level,细化到 function level。在函数里,通过 label 增强函数表达能力。

可能我们一时半会儿,走不到上面那一步。Vue 在 Ref-sugar 里,前进一小步,验证一下,也算一个好事。

七、总结

以上,这篇文章,并不是想说 Vue Ref-sugar 一定是正确的路子,我们要无条件支持它。

而是想强调,Vue Ref-sugar 的做法和设计,并没有部分批评者描述的那么可怕和危险。它看起来,更多的是水到渠成的一小步。这种做法和设计,在其它语言和工程里,也不鲜见。即便是在前端领域,Vue 也不是第一个这样做的。

Vue Ref-sugar 是 opt-in 的,既没有强制开发者使用它,也没有阻碍开发者使用他们喜欢的、熟悉的模式。

如果在标准之下,能找到满足要求的解决方案,自然是最好,不必冒险探索。

但是,在找到那种完美方案之前,我们也不能放弃眼前可以把握到的优化,停下来,无所作为。

即便 Vue Ref-sugar 最终被证明是错误的路线,从它的影响范围和处理方式来看,也不是不可回头的。可以很容易通过 codemod,批量的、自动的将所有 Vue Ref-sugar 代码,转换成原始形态,或者新发掘的形态。

同时,我们也应该更加包容,允许框架在一定程度上犯错。很难奢望永不犯错的完美框架,但是我们可以期待一个会自我纠错的成长型框架。

对于这样一个可选的、可撤销的、谨慎的、有前人成功探索经验的提案,我没有想到会被如此激烈反对和批评。

我感觉,根源上,可能是大家对语言标准和框架之间的关系的理解,出现了差异。

或许在很多开发者心里,框架 <= 语言。

比如,Vue, React,Angular 和 Svelte 是 JS 语言里的几个框架,它们属于 JS 生态的子集,它们在 JS 范畴的约束下。

Vue Ref-sugar 在这个角度,就成了挑战权威,自寻死路。

这种观点,不能说完全错误,但却是比较片面的。

框架其实比我们想象的定义更大,大到:框架 > 语言。

框架面向的是特定领域的解决方案,所有语言和工具,都是它可以选择的手段。

有时,它选择某一门语言作为主要工具,甚至所有相关软件都用同一门语言去实现。但这不意味着,这门语言就凌驾于框架之上。

放到 Vue 角度来说,JS 只是它的一个 runtime 语言选型,实际上,Vue 3.0 选用的编写语言,已经是 TypeScript。将来,Vue 可以由其它语言编写,通过 compile-to-js 或者 WASM 等方式,运行在 Web/Node.js 平台,以及运行在其它 Native 平台。

Vue 里也使用了 HTML,CSS 以及自定义的 Template 等语法。Web 开发本身就是一个多门语言协作的模式,任意一门语言都很难说凌驾于谁之上。

在其中,JavaScript 只是其中一门语言罢了。当 JS 可以很好地解决问题时,Vue 选择它编写代码。当 TS 能更好地解决问题时,Vue 选择 TS。

当 JS 原始语义可以很好地解决问题时,Vue 选择保持原始语义。当 JS 增强语义可以很好地解决问题时,Vue 选择增强 labeled statement 语句的语义。

Vue 面对的是 UI 开发的领域问题,它不是 JS 的附属,反而 JS 是它用以解决领域问题的其中一个工具和手段。

Vue Ref-sugar 提案,在这个角度下,就成了一个自然而然的事情。

为了解决问题,包括 JS/TS/Template 等一切技术和工具在内,都是可调整的。关键要看,成本和收益。

只要收益从长期来看,大于甚至远大于成本,那这个方案就是可用的。

如果批评者愿意仔细阅读尤小右的解释和回复,从成本-收益角度去理解,很容易发现,选择 label statement 是一个充分考虑了工具链适配成本、学习成本、使用成本等多方面的务实选择。

框架作者,比写标准和规范的人,离开发者更近,更关心开发者的诉求。

开发者的诉求,先被框架作者所吸纳,而后写标准和规范的人,还要去跟框架作者们沟通与合作,提供可以满足框架后续发展需求的新特性可能。

框架迫切想得到的特性,甚至还会直接贡献代码给上游(如 Facebook 给 Google Chrome 贡献代码,实现了 React 框架需要的 isInputPending api)。

选择相信有诚意的、有责任心的框架作者们,比盲目崇拜标准和规范,对我们来说,更加有意义。

现在,再去看一遍 Vue Ref-sugar 提案,不知道是否有不同体验呢:——)

[0] New script setup and ref sugar
New script setup and ref sugar by yyx990803 · Pull Request #222 · vuejs/rfcs
[1] 如何评价 Vue 的 ref 语法糖提案?
[2] proposal-refs
rbuckton/proposal-refs
[3] Decorators: A New Proposal
Decorators: A New Proposal
[4] ECMAScript® 2021 Language Specification
tc39.es/ecma262/#
[5] Wiki: Static Semantics
en.wikipedia.org/wiki/P
[6] One-shot Delimited Continuations with Effect Handlers
One-shot Delimited Continuations with Effect Handlers
[7] Swift: Shorthand Setter Declaration
The Swift Programming Language
编辑于 2020-11-12 11:38