Skip to content

响应性语法转换 ​

已移除的实验性特性

Reactivity Transform 曾是一个实验性特性,并且已经在最新的 3.4 版本中移除。请阅读这里的原因说明。

如果你仍然打算使用它,现在可以通过 Vue Macros 插件来使用。

仅适用于 Composition API

Reactivity Transform 是一个仅适用于 Composition API 的特性,并且需要构建步骤。

Refs 与响应式变量 ​

自从 Composition API 引入以来,一个长期悬而未决的主要问题就是该使用 refs 还是 reactive 对象。对 reactive 对象进行解构时很容易丢失响应性,而使用 refs 时又可能不得不在各处都写 .value,这也会显得很繁琐。另外,如果没有类型系统,.value 也很容易被遗漏。

Vue Reactivity Transform 是一种编译时转换,它允许我们写出如下代码:

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 的 refs。例如,上面组件的 <script> 部分会被编译为:

js
import { ref } from 'vue'

let count = ref(0)

console.log(count.value)

function increment() {
  count.value++
}

所有返回 refs 的响应式 API 都会有一个以 $ 为前缀的等价宏。这些 API 包括:

这些宏是全局可用的,在启用 Reactivity Transform 时无需导入,但如果你希望更显式一些,也可以选择从 vue/macros 中导入它们:

js
import { $ref } from 'vue/macros'

let count = $ref(0)

使用 $() 进行解构 ​

组合式函数返回一个 refs 对象,并通过解构来取出这些 refs,是很常见的用法。为此,响应性语法转换提供了 $() 宏:

js
import { useMouse } from '@vueuse/core'

const { x, y } = $(useMouse())

console.log(x, y)

编译结果:

js
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 中,从而使其余代码按预期运行。

$() 的解构既适用于响应式对象,也适用于包含 refs 的普通对象。

使用 $() 将已有 refs 转换为响应式变量 ​

在某些情况下,我们可能会把函数包装起来,而这些函数也会返回 refs。然而,Vue 编译器无法提前知道某个函数会返回 ref。在这种情况下,也可以使用 $() 宏把已有 refs 转换为响应式变量:

js
function myCreateRef() {
  return ref(0)
}

let count = $(myCreateRef())

响应式 Props 解构 ​

当前在 <script setup> 中使用 defineProps() 有两个痛点:

  1. 和 .value 类似,你需要始终通过 props.x 来访问 props,才能保留响应性。这意味着你不能解构 defineProps,因为解构后的变量并不是响应式的,也不会更新。

  2. 使用 仅类型声明的 props 声明 时,没有简单的方法为 props 声明默认值。我们曾专门为此引入 withDefaults() API,但使用起来仍然比较笨重。

当 defineProps 与解构一起使用时,我们可以应用编译时转换来解决这些问题,类似于前面看到的 $():

html
<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>

上面的代码会被编译为如下等价的运行时声明:

js
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 作为参数,例如:

ts
function trackChange(x: Ref<number>) {
  watch(x, (x) => {
    console.log('x changed!')
  })
}

let count = $ref(0)
trackChange(count) // 不起作用!

上面的情况不会按预期工作,因为它会被编译为:

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

这里传入的 count.value 是一个数字,而 trackChange 期望的是一个真正的 ref。可以在传递之前先用 $$() 包裹 count 来修复这个问题:

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

上面的代码会被编译为:

js
import { ref } from 'vue'

let count = ref(0)
trackChange(count)

可以看到,$$() 是一个充当逃逸提示的宏:$$() 中的响应式变量不会再附加 .value。

在函数作用域内返回 ​

如果在返回表达式中直接使用响应式变量,也会导致响应性丢失:

ts
function useMouse() {
  let x = $ref(0)
  let y = $ref(0)

  // 监听 mousemove...

  // 不起作用!
  return {
    x,
    y
  }
}

上面的 return 语句会被编译为:

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

为了保留响应性,我们应该返回真正的 refs,而不是返回时刻的当前值。

同样地,我们可以使用 $$() 来修复这个问题。在这种情况下,$$() 可以直接用于返回的对象上——$$() 调用内部对响应式变量的任何引用,都会保留对其底层 refs 的引用:

ts
function useMouse() {
  let x = $ref(0)
  let y = $ref(0)

  // 监听 mousemove...

  // 已修复
  return $$({
    x,
    y
  })
}

在解构后的 props 上使用 $$() ​

$$() 也适用于解构后的 props,因为它们同样是响应式变量。编译器会为了效率将其转换为 toRef:

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

passAsRef($$(count))

会被编译为:

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

TypeScript 集成 ​

Vue 为这些宏提供了类型定义(全局可用),并且所有类型都会按预期工作。它与标准 TypeScript 语义没有任何不兼容之处,因此这种语法可以与所有现有工具链配合使用。

这也意味着,这些宏可以在任何允许合法 JS / TS 的文件中使用——不仅仅是在 Vue SFC 中。

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

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

当你从 vue/macros 中显式导入这些宏时,无需声明全局类型也可以正常工作。

显式启用 ​

核心中已不再支持

以下内容仅适用于 Vue 3.3 及以下版本。Vue core 3.4 及以上版本以及 @vitejs/plugin-vue 5.0 及以上版本已移除此支持。如果你打算继续使用该转换,请迁移到 Vue Macros。

Vite ​

  • 需要 @vitejs/plugin-vue@>=2.0.0
  • 适用于 SFCs 以及 js(x)/ts(x) 文件。在应用转换前,会先对文件进行快速的使用情况检查,因此对于未使用这些宏的文件不会有性能开销。
  • 注意 reactivityTransform 现在是插件根级别选项,而不是嵌套在 script.refSugar 下,因为它影响的不只是 SFCs。
vite.config.js
js
export default {
  plugins: [
    vue({
      reactivityTransform: true
    })
  ]
}

vue-cli ​

  • 当前仅影响 SFCs
  • 需要 vue-loader@>=17.0.0
vue.config.js
js
module.exports = {
  chainWebpack: (config) => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap((options) => {
        return {
          ...options,
          reactivityTransform: true
        }
      })
  }
}

普通 webpack + vue-loader ​

  • 当前仅影响 SFCs
  • 需要 vue-loader@>=17.0.0
webpack.config.js
js
module.exports = {
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          reactivityTransform: true
        }
      }
    ]
  }
}
响应性语法转换 has loaded