其实之前Vue3
做过好多次语法糖的提案,最经典的莫过于提案。但一开始这个提案夹杂着
ref
语法糖,所以很多批评的声音接踵而来:什么Vue
又开始创造新概念啦、不忠于JavsScript
啦、不如叫之类的…
但尤雨溪
发现反对的意见大多数是对ref
语法糖不满,于是继续细分,把和
ref
语法糖分成了两个不同的提案,如果不太清楚我说的到底是什么东西的话,可以点进这两篇文章看一看:《[译]尤雨溪: Ref语法糖提案》、《Vue 3.0.3 : 新增CSS变量注入以及最新的Ref提案》
最近我看到这个提案终于定稿了,已经进入
Vue
的标准里面去了,我们在用新版Vue
的时候是默认支持这种写法的。不过由于ref
这个提案反对意见太多,尤大怕如果不顾大家的反对意见坚决推进的话,可能会失去大家的信任从而流失一批用户、顺便再给自己多招点黑…
于是ref
这个提案就被放弃掉了。正当我以为终于不用再搞那些花里胡哨的玩意之后,新版的ref
语法糖提案又来了… 原来尤大解决ref
的.value
属性这个决心一直都没有改变,你们不同意原来的写法?那好,换个语法再来一遍!
自从引入 Composition API
以来,一个主要未解决的问题是 ref
对象的使用。.value
在任何地方使用都可能很麻烦,如果不使用 TS 的话,很容易就会忘记写这个.value
属性,就像这样:
import { ref } from 'vue'
let loading = ref(true)
if (loading) {
// 此处省略若干代码
loading = false
}
但实际上我们要写成这样才会正确运行:
if (loading.value) {
// 此处省略若干代码
loading.value = false
}
这就很烦,所以一些用户特别倾向于只用reactive()
这个函数,这样他们就不必面对ref
的.value
属性了,就像这样:
import { reactive } from 'vue'
const state = reactive({
loading: true
})
if (state.loading) {
// 此处省略若干代码
state.loading = false
}
但其实这些写法在尤雨溪
的眼里都不是最好的解决方案,于是他参考了Svelte
的写法,用了几乎快被废弃掉的label
语法:
ref: loading = true
if (loading) {
// 此处省略若干代码
loading = false
}
这个语法为何遭到大家的强烈反对呢?因为我们声明一个变量通常会用let
、const
以及var
关键字对吧,但这个压根儿就没用到任何声明的关键字,取而代之的是不伦不类的ref:
。这个语法并不是尤雨溪自创的啊,它是JS
里的label
语法,但几乎没人用,可能有一部分人听都没听过,它主要是在多重嵌套的循环中配合break
及continue
使用的,就像这样:
let num = 0
outermost:
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
continue outermost
} else {
console.log(i, j, 88)
}
num++
}
}
console.log(num) //95
看不懂没关系啊,也没必要弄懂这种语法,因为它不够直观,用处也不是很大,所以几乎没什么人用它!不过既然没什么人在用,同时它还是JS
的合法语法,那用它来告诉编译器这里是声明了一个ref
变量岂不是很完美?
那么大家为何会如此反对呢?就是因为label
语法压根儿就不是这么用的,人家原本是为了和break
、continue
配合使用的,虽然在别的地方用也不算是语法错误,但你这么做明显是修改了JS
原本的语意!
那尤大新提的这个ref
语法糖长什么样呢,我们来看一下:
<script setup>
let loading = $ref(true)
if (loading) {
// 此处省略若干代码
loading = false
}
script>
尤大心想:你们不是嫌我之前用了不规范的语法么?那我这回这么写应该没问题了吧!想想之前我们定义一个ref
变量,首先需要先把ref
引进来然后才能用:
import { ref } from 'vue'
const loading = ref(true)
而新语法不用引,直接就能用,类似于全局变量的感觉。除了$ref
这个特殊的全局变量呢,这次提案还有:$computed
、$fromRefs
和$raw
这几个玩意。我们一个个来看,先看$computed
:
<script setup>
import { ref, computed } from 'vue'
const num = ref(1)
const num_10 = computed(() => num.value * 10)
script>
<script setup>
let num = $ref(1)
const num_10 = $computed(() => num * 10)
script>
$fromRefs
又是个啥呢?这玩意在之前没有啊!只听说过toRefs
:
<script setup>
import { fromRefs } from 'vue' // 这个API并不存在
import { toRefs } from 'vue' // 这个API倒是有 也就是只有 to 没有 from
script>
其实这个$fromRefs
正是为了配合toRefs
而产生的,比方说我们在别的地方写了一个useXxx
:
import { reactive } from 'vue'
const state = reactive({
x: 0,
y: 0
})
export default = (x = 0, y = 0) => {
state.x = x
state.y = y
return toRefs(state)
}
于是我们在使用的时候就:
<script setup>
import { useXxx } form '../useXxx.js'
const { x, y } = useXxx(100, 200)
console.log(x.value, y.value)
script>
这岂不是又要出现尤大最不想看到的.value
属性了吗?所以$fromRefs
就是为了解决这个问题而生的:
<script setup>
import { useXxx } form '../useXxx.js'
const { x, y } = $fromRefs(useXxx(100, 200))
console.log(x, y)
script>
最后一个 API 就是$raw
了,raw 不是原始的意思嘛!那么看名字也能猜到,就是我们用$ref
所创建出来的其实是一个响应式对象
,而不是一个基本数据类型,但语法糖会让我们在使用的过程中像是在用基本数据类型那样可以改来改去,但有时我们想看看这个对象
长什么样,那么我们就需要用到$raw
了:
<script setup>
const loading = $ref(true)
console.log(loading) // 其实打印的不是 loading 这个对象 而是它里面的值 相当于 loading.value
console.log($raw(loading)) // 这回打印的就是 loading 这个对象了
script>
从技术上来讲,$ref
可以在任何地方被let
声明使用,包括嵌套函数范围:
function useMouse() {
let x = $ref(0)
let y = $ref(0)
function update(e) {
x = e.pageX
y = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return $raw({
x,
y
})
}
上面的代码将会被编译成这个样子:
import { ref } from 'vue'
function useMouse() {
let x = ref(0)
let y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return {
x,
y
}
}
不过目前尚不支持这种写法,仅支持不在函数或者其他块级作用域中的ref
语法糖。
这种语法糖本质上是可以通过babel
等编译工具来转换成任何合法的JS
或TS
代码的,但新语法目前仅支持写在的单文件组件里,这是因为:
JS
或TS
语法,但它毕竟不是标准JS
语义。JS
里并没有$ref
、$computed
这种全局变量。在单文件组件中的
加上一个setup
属性就是用来表示里面的代码将会被预处理一些特殊行为。@vue/compiler-sfc
这个模块的其中一部分,所以它允许现有的Vue
用户在开始使用新语法时不需要任何额外的babel
等配置。
的编译过程已经实现全AST
解析,所以ref
语法糖的变换可以重复使用相同的AST
,并避免产生额外的解析开销。当我们不在单文件组件内写代码时会产生一定的心智负担。先前的研究表明,这种心理成本可能实际上减少了没有语法糖时的使用效率。
不同的语法也会产生摩擦,比方说我们想提取或跨组件重用逻辑时(就是我们俗称的hooks
)。
不过幸运的是,由于变换规则相对而言比较简单,用语法糖编写的代码可以通过IDE
插件来自动转换成没有语法糖的样子。
里面的语法了,所以新的ref
语法糖并不会明显增加额外的解析成本。然而,如果应用到所有的JS
或TS
文件中去的话,将会显著增加额外的解析成本。JavaScript
语义,JS
里并没有$ref
、$raw
这种全局变量,让这种语法生效在Vue
的特定环境之外可能是个坏主意。这种语法是随着Vue 3.2
一同发布的,所以我们的Vue
版本至少要大于等于Vue 3.2.0-beta.1
。由于该语法是实验性的,默认是不启用的,我们需要自行配置:
我们需要在根目录下新建一个vue.config.js
,然后在里面写:
module.exports = {
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
return {
...options,
refSugar: true
}
})
}
}
我们需要在根目录下新建一个vite.config.js
,然后在里面写:
import vue from '@vitejs/plugin-vue'
export default {
plugins: [
vue({
script: {
refSugar: true
}
})
]
}
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /.vue$/,
loader: 'vue-loader',
options: {
refSugar: true
}
}
]
}
}
首先这个新语法还是实验性质的,并未进入标准,尽量不要在主要项目中开启,因为实验性语法不一定就会进入标准。第一波ref
语法糖提案被毙掉之后,我看到有人跑到GitHub
上大加吐槽:
翻译:
我注意到 3.2 的测试版已经取消了第一波
ref
语法糖的支持。我非常失望。因为我已经使用ref
语法糖半年多了,据我所知它是vue3
的一部分。
与其他人不同,我认为理解起来或学习起来并不难。
vue3
已经出来快一年了,ref
语法糖都已经9
个月了。我都已经在我的团队中推进了ref
语法糖的使用,它运行良好,以至于我们现在专门使用Composition API
来进行开发。语法糖带来了很多好处,因为.value
真的很无聊,这是与vue2
的Options API
的最大区别,使用语法糖可以不用写.value
就具备响应式的能力和可组合性的魔力。
但是对于我和我的团队来说,这种变化非常糟糕,我们已经广泛使用了
ref
语法糖。我不知道我是不是少数,但我都已经用了半年多了,因为它得到了非常好的IDE
支持(感谢@johnsoncodehk ),而且在用的时候也没发现任何的bug
,无论是对对象结构还是对原始值的访问都很棒。这对我的开发体验来说是一个很大的改进。
我看了一下新的语法糖,和原来的没什么区别,不还是需要编译器做魔术嘛!因为没有了
label
语法导致它看起来更像原生js
,但其实根本就不是。访问原始值和对象结构也变得更加乏味。添加了很多新的API
:$ref
,$computed
,$fromRefs
,$raw
, 不知道以后还会不会有$shallowRef
, 或者$watch
?
也不知道别人会不会接受这个新语法糖提案,但是至少是伤害了原本支持和使用第一波
ref
语法糖的人。由于 3.1.4 现在可以通过选项控制语法糖是否生效,我希望至少能够通过配置保留住第一波语法糖的写法。
尤雨溪在最后说到:
如前所述,本提案中使用的标签语法存在各种缺陷——特别是与标准
JS
行为的语义不匹配,我们正在放弃这个提议。
再次声明一遍:请记住,标记为实验性的功能是用于评估和收集反馈意见的。它们可能随时更改或中断。除非功能的相应 RFC 已合并,否则无法保证 API 的稳定性。
@vue/compiler-sfc
使用实验性功能时的警告应该已经很清楚了。通过选择实验性功能,您承认您愿意在功能更改或删除时重构您的代码。
#369提出了一个新版本的 ref 语法糖,它不依赖于挪用
label
语法,也不需要专门的工具支持。它目前在3.2.0-beta
中发布,并取代了本提案的实现。同样,这也是实验性的,因此上述所有内容也适用于新提案。
所以说尽可能不要在主要项目中使用它,我们可以没事写个 demo
试验一下,或者在自己的个人项目中使用,不然的话很可能就会像上面那位老哥吐槽的那样了…
其他人也觉得谁让你那么用了,既然用了就要承担风险:
你没有考虑到API 是作为实验性质引入的,以便能够根据用户反馈对其进行调整(很多人不喜欢 label 语法)。它使用户能够试验
API
,在某些情况下,这对于API
的体验感至关重要。当你使用实验性功能时,你将接受如果后续版本不兼容的话,你会对原来的代码进行重构甚至不得不将其删除的风险。在API
稳定并合并到RFC
之前,它也不是Vue 的一部分
不过话虽如此,你应该试试新版的
ref
语法糖,然后再来提供反馈。因为说不定你可能更喜欢新版的语法糖而不是现有版本。
也有人支持吐槽的那位老哥:
新一波语法糖提案似乎仍旧令人费解,但这是我们在不改变 JS 原始语法的情况下所能做的最好的事情了(因为有些人总是介意这一点)我同意同时保留新旧两种语法糖。
当然这种新语法肯定是有人喜欢有人讨厌的,我个人是比较反感这个新语法的,如果屏幕前的你喜欢这个新语法的话,那么请跳过我对这段对新语法的吐槽,以免因观点不合产生激烈互喷等情况。
首先我认为最大的弊端就是尤雨溪提出来的:这种语法糖是否要在单文件组件的外部进行支持?
如果仅在单文件组件里支持,我们在外头写hooks
的时候还是要写.value
属性,一会需要写一会不用写的这样不一致的写法很容易写错(虽然有工具提示可以降低错误
)。但还是很烦,而且这边用着ref
函数,到了另一边又变成了$ref
…
如果在所有文件都支持的情况下吧,又不得不用到babel
等工具进行转换,对性能又是个负担。而且有一个很难受的点就是我们还有customRef
这种比较高级的API
,引用官网上的一个案例:
<template>
<input v-model="text" />
template>
<script>
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
}
}
})
}
export default {
setup() {
return {
text: useDebouncedRef('hello')
}
}
}
script>
这种岂不是又要写.value
属性?那在单文件组件里就会出现这个变量需要写.value
,那个变量又不需要写的状况,很容易把人搞的头大。虽说以后对customRef
这种API
可能会单独再出一个$customRef
语法糖,但我觉得就算写了个.value
属性也没啥吧?至于就跟它较上劲了么… 虽说有时候写多了确实会稍微有点烦,但至少还是很容易理解的嘛:用.value
属性触发了Proxy
的getter
或setter
从而引发依赖收集或更新视图等操作。
还有一些其他的API
如:provide
、inject
等,目前的语法糖并未对它们进行兼容,所以还是会出现一会需要.value
一会又不需要的情况。
还有一个最重要的点就是:一个框架的写法老是变来变去的很不利于推广,想想看Vue3.0
和Vue 3.2
之间有多大的差异,这次开了个坏头的话,以后就更加助长了尤大魔改编译的风气。当然他也确实是为了我们好,改的这些东西也是为了我们写起来更加的方便,有的改的也确实是不错,比如:《Vue超好玩的新特性:在CSS中引入JS变量》
还有现在已经定了稿的语法糖,以前我们引入一个组件老需要再注册一遍:
import Xxx from 'Xxx.vue'
export default {
components: {
Xxx
}
}
写多了这样的代码确实有点烦,现在我们只需要引进来就行,不用注册,但这样本质上并没有改变语意,反而新的语法糖明显改变了语意:
let loading = $ref(true)
按理说loading
应该是个Proxy
代理对象,但是它现在却变成了一个布尔类型的值,而且还多出来个莫名其妙的$ref
函数。
当然你可能会说:你不喜欢不用不就得了?话这么说没错,但是你不用不代表别人也不用,有的人用有的人不用,这样的话在语法层面就已经产生了割裂。我们终究是要看别人代码的,有时候是接手一个遗留下来的项目,有时是在GitHub
上看看别人的项目,在有的人用有的人不用的情况下就很难受。
大家怎么认为呢?可以在评论区留个言看看是喜欢这种语法糖的人多还是反对它的人多。
我们把新语法糖的提案地址放在这里:https://github.com/vuejs/rfcs/discussions/369,希望大家可以积极参与并进去评论,但一定要注意的一点是:要用英文!
可能有人会说:都是中国人用什么英文?虽说用英文尤大可以看得懂,但评论区不全是中国人,Vue
还是有相当一批外国粉丝的,而且也不全是美国人,那些不是英国人美国人的开发者,他们如果也只图自己痛快而说自己国家的母语的话,想必我们就没有办法进行沟通了,同时这也会进一步拉近国人在海外的形象:别人都用英文,就你们中国人用自己的语言,不遵守规则。
那可能有人英文水平真的很差,我们可以这样嘛:找到百度翻译,输入中文后翻译成英文,然后再把英文复制过去。虽然这样做翻译的可能不完全准确,但最起码能达到勉强看懂的地步。同时还有一个技巧就是把翻译成英文的句子再翻译回中文,看看有哪些地方的语意发生了明显的变化,我们再针对那个地方重新自己写一遍。
本文首发于公众号:前端学不动