近日,Vue 发明人尤雨溪在 Vue RFCs 下提交了一份新的 Ref 语法糖提案,该提案一经发布便引来了不少争议。

提案内容

这份提案就是在单文件组织(SFC)中引入一个新的script 标签写法,写法为 <script setup>这种写法会自动将所有顶级变量声明暴露给模板(template)使用。其次还会在 <script setup> 中引入了一个消除 ref 的 value 属性的语法糖,该语法糖在编译期间自动将语法糖转为正常代码。

关于为什么这样做?尤雨溪表示,一方面是通过自动暴露顶级变量可以减少代码的冗余度;另一方面,通过 ref: 语法可以让 ref 更高效。

具体案例

例如这样的 HTML 代码:

<script setup>
// 声明一个会被编译到 ref 的变量
ref: count = 1

function inc() {
  // 该变量可像普通的值一样使用,无需 .value
  count++
}

// 用 $ 前缀对应原始的 ref 对象
console.log($count.value)
</script>

<template>
  <button @click="inc">{{ count }}</button>
</template>

就会被编译成这样:


<script setup>
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(1)

    function inc() {
      count.value++
    }

    console.log(count.value)

    return {
      count,
      inc
    }
  }
}
</script>

<template>
  <button @click="inc">{{ count }}</button>
</template>

注意 label: x = 1 仍是有效的 JS 语法(即带标签的赋值语句,相当于直接对 x 赋值,严格模式下如果 x 未定义则会报错),相当于特殊处理了 ref 标签名的语义。

引发争议

针对以上改动,Github 上持反对意见用户较多:

在这里插入图片描述

在这里插入图片描述
大家可以前往:https://github.com/vuejs/rfcs/pull/222 看老外们对这一改动的反馈。

尤雨溪回复

目前 GitHub 上的反馈似乎偏负面,对此尤雨溪在知乎上进行了回复,原文地址:https://www.zhihu.com/question/429036806/answer/1564223482

RFC本身加上原帖里的回复我已经写了大概快上万字的英语了,但看起来知乎很多回答应该是没看完就急着发表意见了,所以还是用中文针对一些常见的疑问和误解解释一下。

看上去不像 JavaScript

ref: count = 1 使用的是标签语法,在 syntax 层面是合法的 JavaScript,而且在非严格模式下是可以正常执行的,甚至语义也是声明了一个名为 count 的全局变量。同时这也是合法的 TypeScript 语法,不会和类型声明混淆(类型声明必然需要 let 和 const)当然这里确实只是语法层面的合法,实际上等于是给 ref: 这个标签赋予了一个不同的语义。标签语法本身是一个极少被使用的功能,实际使用也都是用于标记循环声明(用在 for/while 前面),像例子中 ref: count = 1 这样的用法,其原始语义是毫无用处的,这也是为什么我们认为牺牲这个原始语义来获得响应式的变量声明是一个值得的交换。

为什么用标签语法而不是直接发明新语法

使用标签语法确实是受到了 Svelte 的启发。根本原因在于和 JS 保持 syntax 层面的完全兼容能够尽可能保证现有的 JS 工具生态对接。标签语法能够正确地被 Babel,TS parser/transformer(如 esbuild/swc),Prettier,ESLint 以及任何 IDE 的 JavaScript 语法高亮所直接支持,只有在涉及语义的情况下,如类型推导和 ESLint 变量相关规则才需要针对性的兼容。如果用一个全新的非标准语法,就意味着需要在 parser 层面对上述所有工具进行修改,基本不可行。

类型推导的问题 RFC 里也有专门讨论,有兴趣的可以看原文,这里不赘述。

感觉心智负担变重了

虽然底层是编译到 ref() 的语法糖,但其实对于新人来说根本不需要知道 ref() 的存在就可以使用,因为在不需要获取底层 ref 对象的场景下,通过 ref: 声明的变量心智模型和用 let 声明的变量的心智模型完全一致。你就把 ref: 当成一个响应式的 let 就行了。这个模型已经足够实现大部分入门级别的功能,只有到进阶之后开始学习逻辑抽取复用时,才需要知道 ref() 的概念。

对于已经学习了 Composition API 的用户来说会觉得 “又多了一个概念”,同时由于 RFC 事无巨细地讨论了编译的规则,会产生一种 “心智负担增加了” 的错觉。其实我很久以前用 CoffeeScript,Babel,或是刚开始用 TS 的时候,也有这样的感觉,因为我喜欢用之前先看看这东西编译出来是个什么样子。结果就是看过了这个之后用着上层语法,脑子里忍不住去把它转换成底层语法。但这本质上是我们的大脑在习惯了底层思维方式之后的一种惯性。这种惯性在使用新语法一段时间之后很快就消失了,我们的大脑适应能力还是很强的。如果你开始就不 care 编译出来是个什么结果,就更不会有这个问题(你用 nullish coalescing 或者 decorator 的时候会去想着 babel/TS 编译出来是个什么结果么?)

对于从零开始的用户来说,如上所述 ref: 就是一个能触发响应的 let 而已,学习成本是很低的。具体可以看一个对比:

Options API

<script>
import Foo from './Foo.vue'

export default {
  components: {
    Foo
  },
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

<template>
  <Foo @click="increment">{{ count }}</Foo>
</template>

Composition API (raw)

<script>
import Foo from './Foo.vue'

export default {
  components: {
    Foo
  },
  setup() {
    const count = ref(0)
    const increment = () => { count.value++ }
    return {
      count,
      increment
    }
  }
}
</script>

<template>
  <Foo @click="increment">{{ count }}</Foo>
</template>

Composition API (基于当前提案)

<script setup>
import Foo from './Foo.vue'

ref: count = 0
const increment = () => { count++ }
</script>

<template>
  <Foo @click="increment">{{ count }}</Foo>
</template>

亲自用一下感觉会更直观:antfu/vite-starter-ref-sugar

也可以看一下一个已经实际使用该语法的用户的代码和他的使用体验:https://github.com/vuejs/rfcs/pull/222#issuecomment-723500871

其他一些疑问

review 时没办法一眼看出来这是普通变量还是ref变量。

Vetur 可以给 ref: 声明的变量加上不同的高亮。

在 SFC里用 ref: 语法糖,日后想把逻辑抽到 .ts/.js 中时,又要改成 ref()的形式。这两种风格的不一致也影响了重构的效率。

由于编译转换规则非常简单直接,Vetur 可以提供这样的工作流:选择代码块 -> 右键 “compile ref sugar” -> 剪切 -> 黏贴。可以提供双向转换,完全机械操作,几乎没有脑力损耗。需要一提的是 Svelte 的响应编译模型无法解决这个问题,因为 Svelte 编译出来的代码不是给人维护的代码,且和组件上下文强绑定,所以没法挪到组件外部。这也顺便回答了 “既然都要语法糖了为什么不像 Svelte 那样走完全编译路线“。

这里不否认组件内外风格的切换是会有额外的心智成本的,但这是否能够接受可能会因人而异。因此,ref:语法糖完全是可选的,可以通过编译选项强制关闭。不喜欢的话依然可以在 <script setup> 里使用 raw Composition API。

<script setup>now directly exposes top level bindings to template.
这使得作用域不清晰, 你不知道这个变量会不会在模板里使用

不少人表达了同样的顾虑,但有意思的是如果我们认真思考以下 “知道这个变量会不会在模板里使用“ 真的有什么实际意义吗?如果你的模版没有用到它,那么它的存在也不会产生任何的影响。另外

function setup() {
  let a = 1
  let b = 2

  return () => {
    // 这里可以取到 a 和 b,但可能只用到 a
    console.log(a)
  }
}

语法糖太多,同一个功能好几种写法

整体上,Vue 现在处于一个从 Options API 向 Composition API 过渡的阶段,这是一个不得已为之的情况,因为我们不可能直接废弃掉 Options API。如果你已经在使用 Composition API,那么这个提案中的语法(如果落地的话)将大概率会成为默认推荐的 SFC 书写方式。对于刚刚上手的新人而言,由于目前的文档还是以 Options API 为主,而这个新提案还处在没有文档的讨论阶段,短期内确实可能会导致一定程度的困惑,但从长线来说最终会把文档迁移到直接用这样的语法上手的状态。

最后,尤雨溪还表示:

这个 RFC 提案在发表之前我就知道会引起很多争议,大部分的反应其实都在意料之中,很多提出的所谓问题在 GitHub 上已有的讨论甚至RFC 原文中都已经讨论解释过。如果真的想要对这个提案的具体细节发表看法,还是建议先完整看完 RFC 全文和 GitHub 上的讨论。

其实第一反应 “不能接受” 我是能理解的,不过新技术一开始觉得不能接受最后真香的例子多得很,当初TypeScript,Hooks,Composition API,甚至是 React/JSX 出来的时候不能接受的人也很多。有兴趣的可以看看Pete Hunt 的知名演讲 Rethinking Best Practices

相关链接:

Logo

前往低代码交流专区

更多推荐