跳轉到內容

響應式轉換

移除實驗性功能

響應式轉換是一個實驗性功能,已在最新的 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
        }
      }
    ]
  }
}
響應性轉換已載入