• 首页
  • vue
  • TypeScript
  • JavaScript
  • scss
  • css3
  • html5
  • php
  • MySQL
  • redis
  • jQuery
  • 响应性语法糖

    实验性功能
    响应性语法糖目前是一个实验性功能,默认是禁用的,需要显式选择使用。在最终确定前仍可能发生变化,你可以查看GitHub 上的提案与讨论来关注和跟进最新进展。
    组合式 API 特有
    响应性语法糖是组合式 API 特有的功能,并且需要一个构建步骤。

    ref vs.响应式变量

    自从引入组合式 API 的概念以来,一个主要的未能解决的问题就是ref和响应式对象的使用方式。到处使用.value无疑是很繁琐的,并且在没有类型系统的帮助时很容易漏掉。

    Vue 的响应性语法糖是一个编译时的转换过程,使我们可以像这样书写代码:

    <script setup>
    let count = $ref(0)
    
    console.log(count)
    
    function increment() {
       count++
    }
    </script>
    
    <template>
       <button @click="increment">{{ count }}</button>
    </template>
    

    这里的这个$ref()方法是一个编译时的宏命令:它不是一个真实的、在运行时会调用的方法。而是用作 Vue 编译器的标记,表明最终的count变量需要是一个响应式变量。

    响应式的变量可以像普通变量那样被访问和重新赋值,但这些操作在编译后都会变为带.value的 ref。比如上面例子中<script>部分的代码就被编译成了下面这样:

    import { ref } from 'vue'
    
    let count = ref(0)
    
    console.log(count.value)
    
    function increment() {
      count.value++
    }
    

    每一个会返回ref的响应性 API 都有一个相对应的、以$为前缀的宏函数。包括以下这些 API:

    • ref$ref
    • computed$computed
    • shallowRef$shallowRef
    • customRef$customRef
    • toRef$toRef

    当响应性语法糖时,这些宏函数都是全局可用的、无需手动导入。但如果你想让它更明显,你也可以选择从vue/macros中引入它们:

    import { $ref } from 'vue/macros'
    let count =  $ref(0)
    


    通过$()解构

    我们常常会让一个组合函数返回一个含数个 ref 的对象,然后解构得到这些 ref。对于这种场景,响应性语法糖提供了一个$()宏:

    import { useMouse } from '@vueuse/core'
    
    const { x, y } = $(useMouse())
    
    console.log(x, y)
    

    编译输出为:

    import { toRef } from 'vue'
    import { useMouse } from '@vueuse/core'
    
    const __temp = useMouse(),
      x = toRef(__temp, 'x'),
      y = toRef(__temp, 'y')
    
    console.log(x.value, y.value)
    

    请注意如果x已经是一个 ref,toRef(__temp,'x')则会简单地返回它本身,而不会再创建新的 ref。如果一个被解构的值不是 ref(例如是一个函数),也仍然可以使用,这个值会被包装进一个 ref,因此其他代码都会正常工作。

    $()的解构在响应式对象和包含数个 ref 的对象都可用。


    $()将现存的 ref 转换为响应式对象

    在某些场景中我们可能已经有了会返回 ref 的函数。然而,Vue 编译器并不能够提前知道该函数会返回一个 ref。那么此时可以使用$()宏来将现存的 ref 转换为响应式变量。

    function myCreateRef() {
      return ref(0)
    }
    
    let count = $(myCreateRef())
    


    响应式 props 解构

    现在的<script setup>中对defineProps宏的使用有两个痛点:

    1. .value类似,为了保持响应性,你始终需要以props.x的方式访问这些 prop。这意味着你不能够通过解构defineProps因为得到的变量将不是响应式的、也不会更新。
    2. 当使用基于类型的 props 的声明时,无法很方便地声明这些 prop 的默认值。为此我们提供了withDefaults()这个 API,但使用起来仍然很笨拙。

    而有了响应性语法糖,我们就也可以在defineProps时使用响应式变量相同的解构写法了:

    <script setup lang="ts">
      interface Props {
        msg: string
        count?: number
        foo?: string
      }
    
      const {
        msg,
        // 默认值正常可用
        count = 1,
        // 解构时命别名也可用
        // 这里我们就将 `props.foo` 命别名为 `bar`
        foo: bar
      } = defineProps<Props>()
    
      watchEffect(() => {
        // 会在 props 变化时打印
        console.log(msg, count, bar)
      })
    </script>
    

    上面的代码将被编译成下面这样的运行时声明:

    export default {
      props: {
        msg: { type: String, required: true },
        count: { type: Number, default: 1 },
        foo: String
      },
      setup(props) {
        watchEffect(() => {
          console.log(props.msg, props.count, props.foo)
        })
      }
    }
    


    保持在函数间的响应性

    虽然响应式变量使我们可以不再受.value的困扰,但它也使得我们在函数间传递响应式变量时可能造成“响应性丢失”的问题。这可能在以下两种场景中出现:

    以参数形式传入函数

    假设有一个期望接收一个 ref 对象为参数的函数:

    function trackChange(x: Ref) {
      watch(x, (x) => {
        console.log('x 改变了!')
      })
    }
    
    let count = $ref(0)
    trackChange(count) // 无效
    

    上面的例子不会正常工作,因为代码被编译成了这样:

    let count = ref(0)
    trackChange(count.value)
    

    这里的count.value是以一个 number 类型值的形式传入,然而trackChange期望接收的是一个真正的 ref。要解决这个问题,可以在将count作为参数传入之前,用$$()包装:

    let count = $ref(0)
    - trackChange(count)
    + trackChange($$(count))
    

    上面的代码将被编译成:

    import { ref } from 'vue'
    
    let count = ref(0)
    trackChange(count)
    

    我们可以看到,$$()的效果就像是一个转义标识:$$()中的响应式变量不会追加上.value


    作为函数返回值

    如果将响应式变量直接放在返回值表达式中回丢失掉响应性:

    function useMouse() {
      let x = $ref(0)
      let y = $ref(0)
    
      // 监听 mousemove 事件
      // 不起效!
      return {
        x,
        y
      }
    }
    

    上面的语句将被翻译为:

    return {
      x: x.value,
      y: y.value
    }
    

    为了保持响应性,我们需要返回的是真正的 ref,而不是返回时 ref 内的值。

    我们还是可以使用$$()来结局。在这个例子中,$$()可以直接用在要返回的对象上,$$()调用时任何对响应式变量的引用都会保留为对相应 ref 的引用:

    function useMouse() {
      let x = $ref(0)
      let y = $ref(0)
    
      // 监听 mousemove 事件
      // 修改后起效
      return $$({
        x,
        y
      })
    }
    


    在解构$$()使用

    $$()可以用在对 props 的结构,因为它们也是响应式的变量。编译器会高效地通过toRef来做转换:

    const { count } = defineProps<{ count: number }>()
    
    passAsRef($$(count))
    

    编译结果为:

    setup(props) {
      const __props_count = toRef(props, 'count')
      passAsRef(__props_count)
    }
    


    TypeScript 集成

    Vue 为这些宏函数都提供了类型声明(全局可用)并且类型都会符合你的使用预期。它与标准的 TypeScript 语义没有不兼容之处,因此它的语法可以与所有现有的工具兼容。

    这也意味着这些宏函数在任何 JS / TS 文件中都是合法的,不是仅能在 Vue SFC 中使用。

    因为这些宏函数都是全局可用的,它们的类型需要被显式地引用(例如,在env.d.ts文件中):

    /// <reference types="vue/macros-global" />
    

    若你是从vue/macros中显式引入宏函数时,则不需要像这样全局声明了。


    显式启用

    响应性语法糖目前默认是关闭状态,需要你显式选择启用。此外,接下来的所有配置都需要vue@^3.2.25

    Vite

    • 需要@vitejs/plugin-vue@^2.0.0
    • 应用于 SFC 和 js(x)/ts(x)文件。在执行转换之前,会对文件进行快速的使用检查,因此不使用宏的文件应该不会有性能损失。
    • 注意refTransform现在是一个插件的顶层选项,而不再是位于script.refSugar之中了,因为它不仅仅只对 SFC 起效。
    // vite.config.js
    export default {
      plugins: [
        vue({
          reactivityTransform: true
        })
      ]
    }
    


    vue-cli

    • 目前仅对 SFC 起效
    • 需要vue-loader@^17.0.0
    // vue.config.js
    module.exports = {
      chainWebpack: (config) => {
        config.module
          .rule('vue')
          .use('vue-loader')
          .tap((options) => {
            return {
              ...options,
              reactivityTransform: true
            }
          })
      }
    }
    


    仅用webpack+vue-loader

    • 目前仅对 SFC 起效
    • 需要vue-loader@^17.0.0
    // webpack.config.js
    module.exports = {
      module: {
        rules: [
          {
            test: /\.vue$/,
            loader: 'vue-loader',
            options: {
              reactivityTransform: true
            }
          }
        ]
      }
    }