跳转到内容

响应式转换

移除实验性功能

响应式转换是一个实验性功能,已在最新的 3.4 版本中移除。请阅读有关 移除原因 的信息。

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

Composition-API-specific

响应式转换是 Composition-API-specific 特性,需要构建步骤。

引用与响应式变量的比较

自从 Composition API 介绍以来,一个主要未解决的问题就是引用与响应式对象的使用。在解构响应式对象时,很容易丢失响应性,而当使用引用时,需要在每个地方使用 .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 的引用。例如,上述组件的 <script> 部分被编译成

js
import { ref } from 'vue'

let count = ref(0)

console.log(count.value)

function increment() {
  count.value++
}

每个返回引用的响应式 API 都有一个以 $ 为前缀的宏等效项。这些 API 包括

这些宏在全局范围内可用,当启用 Reactivity Transform 时不需要导入,但您可以可选地从 vue/macros 导入它们,如果您想更加明确的话。

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

let count = $ref(0)

使用 $() 进行解构

通常,组合函数会返回一个引用的对象,并使用解构来检索这些引用。为此,响应式转换提供了 $() 宏。

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 已经是一个引用,则 toRef(__temp, 'x') 将直接返回它,而不会创建额外的引用。如果解构的值不是一个引用(例如函数),它仍然可以工作 - 值将被包裹在一个引用中,以便其余代码按预期工作。

$() 解构在响应式对象 包含引用的普通对象上都有效。

使用 $() 将现有引用转换为响应式变量

在某些情况下,我们可能已经包装了返回引用的函数。然而,Vue 编译器无法事先知道函数将返回一个引用。在这种情况下,也可以使用 $() 宏将任何现有引用转换为响应式变量。

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

let count = $(myCreateRef())

响应式属性解构

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

  1. 类似于 .value,您需要始终以 props.x 的形式访问属性以保持响应性。这意味着您不能解构 defineProps,因为解构后的变量不是响应式的,并且不会更新。

  2. 当使用 仅类型属性声明 时,没有简单的方法来声明属性的默认值。我们引入了 withDefaults() API 用于此目的,但它仍然使用起来很繁琐。

我们可以通过在解构时使用 defineProps 应用编译时转换来解决这些问题,类似于我们之前看到的 $()

html
<script setup lang="ts">
  interface Props {
    msg: string
    count?: number
    foo?: string
  }

  const {
    msg,
    // default value just works
    count = 1,
    // local aliasing also just works
    // here we are aliasing `props.foo` to `bar`
    foo: bar
  } = defineProps<Props>()

  watchEffect(() => {
    // will log whenever the props change
    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,但当我们跨函数边界传递响应式变量时,它会产生“响应性丢失”的问题。这发生在两种情况下

作为参数传递到函数中

给定一个期望将引用作为参数的函数,例如。

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

let count = $ref(0)
trackChange(count) // doesn't work!

上述情况将无法按预期工作,因为它编译为

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)

  // listen to mousemove...

  // doesn't work!
  return {
    x,
    y
  }
}

上述返回语句编译为

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

为了保持响应性,我们应该返回实际的refs,而不是在返回时的当前值。

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

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

  // listen to mousemove...

  // fixed
  return $$({
    x,
    y
  })
}

在解构属性上使用$$()

$$()对解构属性也有效,因为它们也是响应式变量。编译器将使用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 SFCs。

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

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

当从vue/macros显式导入宏时,类型将无需声明全局即可正常工作。

显式选择

不再在核心中支持

以下仅适用于 Vue 版本 3.3 及以下。支持已在 Vue 核心版本 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。
js
// vite.config.js
export default {
  plugins: [
    vue({
      reactivityTransform: true
    })
  ]
}

vue-cli

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

Plain webpack + vue-loader

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