乐趣区

关于前端:Vue第二波ref语法提案来袭-这次会进入到标准吗

前言

其实之前 Vue3 做过好屡次语法糖的提案,最经典的莫过于 <script setup> 提案。但一开始这个提案夹杂着 ref 语法糖,所以很多批评的声音接踵而来:什么 Vue 又开始发明新概念啦、不忠于 JavsScript 啦、不如叫 <script lang="vue-script"> 之类的…

尤雨溪 发现拥护的意见大多数是对 ref 语法糖不满,于是持续细分,把 <script setup>ref语法糖分成了两个不同的提案,如果不太分明我说的到底是什么货色的话,能够点进这两篇文章看一看:《[译]尤雨溪: Ref 语法糖提案》、《Vue 3.0.3 : 新增 CSS 变量注入以及最新的 Ref 提案》

最近我看到 <script setup> 这个提案终于定稿了,曾经进入 Vue 的规范外面去了,咱们在用新版 Vue 的时候是默认反对这种写法的。不过因为 ref 这个提案拥护意见太多,尤大怕如果不顾大家的拥护意见坚定推动的话,可能会失去大家的信赖从而散失一批用户、顺便再给本人多招点黑…

于是 ref 这个提案就被放弃掉了。正当我认为终于不必再搞那些花里胡哨的玩意之后,新版的 ref 语法糖提案又来了… 原来尤大解决 ref.value属性这个信心始终都没有扭转,你们不批准原来的写法?那好,换个语法再来一遍!

为什么老想做这个 ref 语法糖?

自从引入 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
}

这个语法为何受到大家的强烈拥护呢?因为咱们申明一个变量通常会用 letconst 以及 var 关键字对吧,但这个压根儿就没用到任何申明的关键字,取而代之的是不三不四的 ref:。这个语法并不是尤雨溪借鉴的啊,它是JS 里的 label 语法,但简直没人用,可能有一部分人听都没听过,它次要是在多重嵌套的循环中配合 breakcontinue应用的,就像这样:

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 语法压根儿就不是这么用的,人家本来是为了和 breakcontinue 配合应用的,尽管在别的中央用也不算是语法错误,但你这么做显著是批改了 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 等编译工具来转换成任何非法的 JSTS代码的,但新语法目前仅反对写在 <script setup> 的单文件组件里,这是因为:

  • 只管是语法上无效的 JSTS语法,但它毕竟不是规范 JS 语义。JS里并没有 $ref$computed 这种全局变量。在单文件组件中的 <script> 加上一个 setup 属性就是用来示意外面的代码将会被预处理一些非凡行为。
  • 因为它被实现为 @vue/compiler-sfc 这个模块的其中一部分,所以它容许现有的 Vue 用户在开始应用新语法时不须要任何额定的 babel 等配置。
  • <script setup>的编译过程曾经实现全 AST 解析,所以 ref 语法糖的变换能够重复使用雷同的AST,并防止产生额定的解析开销。
  • 新语法的转换还会被编译器进行智能绑定。

    如果新语法仅限于单文件组件

    当咱们不在单文件组件内写代码时会产生肯定的心智累赘。先前的钻研表明,这种心理老本可能实际上缩小了没有语法糖时的应用效率。

不同的语法也会产生摩擦,比方说咱们想提取或跨组件重用逻辑时 ( 就是咱们俗称的hooks)。

不过侥幸的是,因为变换规定相对而言比较简单,用语法糖编写的代码能够通过 IDE 插件来主动转换成没有语法糖的样子。

新语法如果反对所有文件

  • 解析老本:咱们曾经解析 <script setup> 外面的语法了,所以新的 ref 语法糖并不会明显增加额定的解析老本。然而,如果利用到所有的 JSTS文件中去的话,将会显著减少额定的解析老本。
  • 这种新语法并不是规范的 JavaScript 语义,JS里并没有 $ref$raw 这种全局变量,让这种语法失效在 Vue 的特定环境之外可能是个坏主意。

    如何开启新语法?

    这种语法是随着 Vue 3.2 一起公布的,所以咱们的 Vue 版本至多要大于等于Vue 3.2.0-beta.1。因为该语法是实验性的,默认是不启用的,咱们须要自行配置:

在 vue-cli 脚手架中

咱们须要在根目录下新建一个vue.config.js,而后在外面写:

module.exports = {
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => {
        return {
          ...options,
          refSugar: true
        }
      })
  }
}

在 Vite 中

咱们须要在根目录下新建一个vite.config.js,而后在外面写:

import vue from '@vitejs/plugin-vue'

export default {
  plugins: [
    vue({
      script: {refSugar: true}
    })
  ]
}

在本人搭建的 webpack 脚手架中

// 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 真的很无聊,这是与 vue2Options 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 属性触发了 Proxygettersetter 从而引发依赖收集或更新视图等操作。

还有一些其余的 API 如:provideinject 等,目前的语法糖并未对它们进行兼容,所以还是会呈现一会须要 .value 一会又不须要的状况。

还有一个最重要的点就是:一个框架的写法老是变来变去的很不利于推广,想想看 Vue3.0Vue 3.2之间有多大的差别,这次开了个坏头的话,当前就更加助长了尤大魔改编译的风尚。当然他也的确是为了咱们好,改的这些货色也是为了咱们写起来更加的不便,有的改的也的确是不错,比方:《Vue 超好玩的新个性:在 CSS 中引入 JS 变量》

还有当初曾经定了稿的 <script setup> 语法糖,以前咱们引入一个组件老须要再注册一遍:

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还是有相当一批外国粉丝的,而且也不全是美国人,那些不是英国人美国人的开发者,他们如果也只图本人畅快而说本人国家的母语的话,想必咱们就没有方法进行沟通了,同时这也会进一步拉近国人在海内的形象:他人都用英文,就你们中国人用本人的语言,不遵守规则。

那可能有人英文程度真的很差,咱们能够这样嘛:找到百度翻译,输出中文后翻译成英文,而后再把英文复制过来。尽管这样做翻译的可能不齐全精确,但最起码能达到勉强看懂的境地。同时还有一个技巧就是把翻译成英文的句子再翻译回中文,看看有哪些地方的语意产生了显著的变动,咱们再针对那个中央从新本人写一遍。

本文首发于公众号:前端学不动

退出移动版