Vue3 組合式 API 大全

Vue3 API 匯總手冊

setup

setup 函數(shù)是一個新的組件選項远豺。作為在組件內(nèi)使用 Composition API 的入口點窑滞。

  • 調(diào)用時機

    創(chuàng)建組件實例良漱,然后初始化 props 毅弧,緊接著就調(diào)用setup 函數(shù)。從生命周期鉤子的視角來看喜爷,它會在 beforeCreate 鉤子之前被調(diào)用

  • 模板中使用

    如果 setup 返回一個對象冗疮,則對象的屬性將會被合并到組件模板的渲染上下文:

    <template>
      <div>{{ count }} {{ object.foo }}</div>
    </template>
    
    <script>
      import { ref, reactive } from 'vue'
    
      export default {
        setup() {
          const count = ref(0)
          const object = reactive({ foo: 'bar' })
    
          // 暴露給模板
          return {
            count,
            object,
          }
        },
      }
    </script>
    

    注意 setup 返回的 ref 在模板中會自動解開,不需要寫 .value贞奋。

  • 渲染函數(shù) / JSX 中使用

    setup 也可以返回一個函數(shù)赌厅,函數(shù)中也能使用當(dāng)前 setup 函數(shù)作用域中的響應(yīng)式數(shù)據(jù):

    import { h, ref, reactive } from 'vue'
    
    export default {
      setup() {
        const count = ref(0)
        const object = reactive({ foo: 'bar' })
    
        return () => h('div', [count.value, object.foo])
      },
    }
    
  • 參數(shù)

    該函數(shù)接收 props 作為其第一個參數(shù):

    export default {
      props: {
        name: String,
      },
      setup(props) {
        console.log(props.name)
      },
    }
    

    注意 props 對象是響應(yīng)式的,watchEffectwatch 會觀察和響應(yīng) props 的更新:

    export default {
      props: {
        name: String,
      },
      setup(props) {
        watchEffect(() => {
          console.log(`name is: ` + props.name)
        })
      },
    }
    

    然而不要解構(gòu) props 對象轿塔,那樣會使其失去響應(yīng)性:

    export default {
      props: {
        name: String,
      },
      setup({ name }) {
        watchEffect(() => {
          console.log(`name is: ` + name) // Will not be reactive!
        })
      },
    }
    

    在開發(fā)過程中特愿,props 對象對用戶空間代碼是不可變的(用戶代碼嘗試修改 props 時會觸發(fā)警告)仲墨。

    第二個參數(shù)提供了一個上下文對象,從原來 2.x 中 this 選擇性地暴露了一些 property揍障。

    const MyComponent = {
      setup(props, context) {
        context.attrs
        context.slots
        context.emit
      },
    }
    

    attrsslots 都是內(nèi)部組件實例上對應(yīng)項的代理目养,可以確保在更新后仍然是最新值。所以可以解構(gòu)毒嫡,無需擔(dān)心后面訪問到過期的值:

    const MyComponent = {
      setup(props, { attrs }) {
        // 一個可能之后回調(diào)用的簽名
        function onClick() {
          console.log(attrs.foo) // 一定是最新的引用癌蚁,沒有丟失響應(yīng)性
        }
      },
    }
    

    出于一些原因?qū)?props 作為第一個參數(shù),而不是包含在上下文中:

    • 組件使用 props 的場景更多兜畸,有時候甚至只使用 props
    • props 獨立出來作為第一個參數(shù)努释,可以讓 TypeScript 對 props 單獨做類型推導(dǎo),不會和上下文中的其他屬性相混淆咬摇。這也使得 setup 伐蒂、 render 和其他使用了 TSX 的函數(shù)式組件的簽名保持一致。
  • this的用法

    thissetup() 中不可用肛鹏。由于 setup() 在解析 2.x 選項前被調(diào)用逸邦,setup() 中的 this 將與 2.x 選項中的 this 完全不同。同時在 setup() 和 2.x 選項中使用 this 時將造成混亂在扰。在 setup() 中避免這種情況的另一個原因是:這對于初學(xué)者來說缕减,混淆這兩種情況的 this 是非常常見的錯誤:

    setup() {
      function onClick() {
        this // 這里 `this` 與你期望的不一樣!
      }
    }
    
  • 類型定義

    interface Data {
      [key: string]: unknown
    }
    
    interface SetupContext {
      attrs: Data
      slots: Slots
      emit: (event: string, ...args: unknown[]) => void
    }
    
    function setup(props: Data, context: SetupContext): Data
    

    提示

    為了獲得傳遞給 setup() 參數(shù)的類型推斷芒珠,需要使用 defineComponent桥狡。

#響應(yīng)式系統(tǒng) API

#reactive

接收一個普通對象然后返回該普通對象的響應(yīng)式代理。等同于 2.x 的 Vue.observable()

const obj = reactive({ count: 0 })

響應(yīng)式轉(zhuǎn)換是“深層的”:會影響對象內(nèi)部所有嵌套的屬性妓局∽芊牛基于 ES2015 的 Proxy 實現(xiàn)呈宇,返回的代理對象不等于原始對象好爬。建議僅使用代理對象而避免依賴原始對象。

  • 類型定義

    function reactive<T extends object>(raw: T): T
    

#ref

接受一個參數(shù)值并返回一個響應(yīng)式且可改變的 ref 對象甥啄。ref 對象擁有一個指向內(nèi)部值的單一屬性 .value存炮。

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

如果傳入 ref 的是一個對象,將調(diào)用 reactive 方法進行深層響應(yīng)轉(zhuǎn)換蜈漓。

  • 模板中訪問

    當(dāng) ref 作為渲染上下文的屬性返回(即在setup() 返回的對象中)并在模板中使用時穆桂,它會自動解套,無需在模板內(nèi)額外書寫 .value

    <template>
      <div>{{ count }}</div>
    </template>
    
    <script>
      export default {
        setup() {
          return {
            count: ref(0),
          }
        },
      }
    </script>
    
  • 作為響應(yīng)式對象的屬性訪問

    當(dāng) ref 作為 reactive 對象的 property 被訪問或修改時融虽,也將自動解套 value 值享完,其行為類似普通屬性:

    const count = ref(0)
    const state = reactive({
      count,
    })
    
    console.log(state.count) // 0
    
    state.count = 1
    console.log(count.value) // 1
    

    注意如果將一個新的 ref 分配給現(xiàn)有的 ref, 將替換舊的 ref:

    const otherCount = ref(2)
    
    state.count = otherCount
    console.log(state.count) // 2
    console.log(count.value) // 1
    

    注意當(dāng)嵌套在 reactive Object 中時有额,ref 才會解套般又。從 Array 或者 Map 等原生集合類中訪問 ref 時彼绷,不會自動解套:

    const arr = reactive([ref(0)])
    // 這里需要 .value
    console.log(arr[0].value)
    
    const map = reactive(new Map([['foo', ref(0)]]))
    // 這里需要 .value
    console.log(map.get('foo').value)
    
  • 類型定義

    interface Ref<T> {
      value: T
    }
    
    function ref<T>(value: T): Ref<T>
    

    有時我們可能需要為 ref 做一個較為復(fù)雜的類型標(biāo)注。我們可以通過在調(diào)用 ref 時傳遞泛型參數(shù)來覆蓋默認推導(dǎo):

    const foo = ref<string | number>('foo') // foo 的類型: Ref<string | number>
    
    foo.value = 123 // 能夠通過茴迁!
    

#computed

傳入一個 getter 函數(shù),返回一個默認不可手動修改的 ref 對象。

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 錯誤抚官!

或者傳入一個擁有 getset 函數(shù)的對象桂对,創(chuàng)建一個可手動修改的計算狀態(tài)。

const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => {
    count.value = val - 1
  },
})

plusOne.value = 1
console.log(count.value) // 0
  • 類型定義

    // 只讀的
    function computed<T>(getter: () => T): Readonly<Ref<Readonly<T>>>
    
    // 可更改的
    function computed<T>(options: {
      get: () => T
      set: (value: T) => void
    }): Ref<T>
    

#readonly

傳入一個對象(響應(yīng)式或普通)或 ref倦卖,返回一個原始對象的只讀代理洒擦。一個只讀的代理是“深層的”,對象內(nèi)部任何嵌套的屬性也都是只讀的怕膛。

const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
  // 依賴追蹤
  console.log(copy.count)
})

// original 上的修改會觸發(fā) copy 上的偵聽
original.count++

// 無法修改 copy 并會被警告
copy.count++ // warning!

#watchEffect

立即執(zhí)行傳入的一個函數(shù)秘遏,并響應(yīng)式追蹤其依賴,并在其依賴變更時重新運行該函數(shù)嘉竟。

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> 打印出 0

setTimeout(() => {
  count.value++
  // -> 打印出 1
}, 100)

#停止偵聽

當(dāng) watchEffect 在組件的 setup() 函數(shù)或生命周期鉤子被調(diào)用時邦危, 偵聽器會被鏈接到該組件的生命周期,并在組件卸載時自動停止舍扰。

在一些情況下倦蚪,也可以顯式調(diào)用返回值以停止偵聽:

const stop = watchEffect(() => {
  /* ... */
})

// 之后
stop()

#清除副作用

有時副作用函數(shù)會執(zhí)行一些異步的副作用, 這些響應(yīng)需要在其失效時清除(即完成之前狀態(tài)已改變了)。所以偵聽副作用傳入的函數(shù)可以接收一個 onInvalidate 函數(shù)作入?yún)? 用來注冊清理失效時的回調(diào)边苹。當(dāng)以下情況發(fā)生時陵且,這個失效回調(diào)會被觸發(fā):

  • 副作用即將重新執(zhí)行時
  • 偵聽器被停止 (如果在 setup() 或 生命周期鉤子函數(shù)中使用了 watchEffect, 則在卸載組件時)
watchEffect((onInvalidate) => {
  const token = performAsyncOperation(id.value)
  onInvalidate(() => {
    // id 改變時 或 停止偵聽時
    // 取消之前的異步操作
    token.cancel()
  })
})

我們之所以是通過傳入一個函數(shù)去注冊失效回調(diào),而不是從回調(diào)返回它(如 React useEffect 中的方式)个束,是因為返回值對于異步錯誤處理很重要慕购。

在執(zhí)行數(shù)據(jù)請求時,副作用函數(shù)往往是一個異步函數(shù):

const data = ref(null)
watchEffect(async () => {
  data.value = await fetchData(props.id)
})

我們知道異步函數(shù)都會隱式地返回一個 Promise茬底,但是清理函數(shù)必須要在 Promise 被 resolve 之前被注冊沪悲。另外,Vue 依賴這個返回的 Promise 來自動處理 Promise 鏈上的潛在錯誤阱表。

#副作用刷新時機

Vue 的響應(yīng)式系統(tǒng)會緩存副作用函數(shù)殿如,并異步地刷新它們,這樣可以避免同一個 tick 中多個狀態(tài)改變導(dǎo)致的不必要的重復(fù)調(diào)用最爬。在核心的具體實現(xiàn)中, 組件的更新函數(shù)也是一個被偵聽的副作用涉馁。當(dāng)一個用戶定義的副作用函數(shù)進入隊列時, 會在所有的組件更新后執(zhí)行:

<template>
  <div>{{ count }}</div>
</template>

<script>
  export default {
    setup() {
      const count = ref(0)

      watchEffect(() => {
        console.log(count.value)
      })

      return {
        count,
      }
    },
  }
</script>

在這個例子中:

  • count 會在初始運行時同步打印出來
  • 更改 count 時,將在組件更新后執(zhí)行副作用爱致。

請注意烤送,初始化運行是在組件 mounted 之前執(zhí)行的。因此糠悯,如果你希望在編寫副作用函數(shù)時訪問 DOM(或模板 ref)帮坚,請在 onMounted 鉤子中進行:

onMounted(() => {
  watchEffect(() => {
    // 在這里可以訪問到 DOM 或者 template refs
  })
})

如果副作用需要同步或在組件更新之前重新運行牢裳,我們可以傳遞一個擁有 flush 屬性的對象作為選項(默認為 'post'):

// 同步運行
watchEffect(
  () => {
    /* ... */
  },
  {
    flush: 'sync',
  }
)

// 組件更新前執(zhí)行
watchEffect(
  () => {
    /* ... */
  },
  {
    flush: 'pre',
  }
)

#偵聽器調(diào)試

onTrackonTrigger 選項可用于調(diào)試一個偵聽器的行為。

  • 當(dāng)一個 reactive 對象屬性或一個 ref 作為依賴被追蹤時叶沛,將調(diào)用 onTrack
  • 依賴項變更導(dǎo)致副作用被觸發(fā)時蒲讯,將調(diào)用 onTrigger

這兩個回調(diào)都將接收到一個包含有關(guān)所依賴項信息的調(diào)試器事件。建議在以下回調(diào)中編寫 debugger 語句來檢查依賴關(guān)系:

watchEffect(
  () => {
    /* 副作用的內(nèi)容 */
  },
  {
    onTrigger(e) {
      debugger
    },
  }
)

onTrackonTrigger 僅在開發(fā)模式下生效灰署。

  • 類型定義

    function watchEffect(
      effect: (onInvalidate: InvalidateCbRegistrator) => void,
      options?: WatchEffectOptions
    ): StopHandle
    
    interface WatchEffectOptions {
      flush?: 'pre' | 'post' | 'sync'
      onTrack?: (event: DebuggerEvent) => void
      onTrigger?: (event: DebuggerEvent) => void
    }
    
    interface DebuggerEvent {
      effect: ReactiveEffect
      target: any
      type: OperationTypes
      key: string | symbol | undefined
    }
    
    type InvalidateCbRegistrator = (invalidate: () => void) => void
    
    type StopHandle = () => void
    

#watch

watch API 完全等效于 2.x this.$watch (以及 watch 中相應(yīng)的選項)判帮。watch 需要偵聽特定的數(shù)據(jù)源,并在回調(diào)函數(shù)中執(zhí)行副作用溉箕。默認情況是懶執(zhí)行的晦墙,也就是說僅在偵聽的源變更時才執(zhí)行回調(diào)。

  • 對比 watchEffect肴茄,watch 允許我們:

    • 懶執(zhí)行副作用晌畅;
    • 更明確哪些狀態(tài)的改變會觸發(fā)偵聽器重新運行副作用;
    • 訪問偵聽狀態(tài)變化前后的值寡痰。
  • 偵聽單個數(shù)據(jù)源

    偵聽器的數(shù)據(jù)源可以是一個擁有返回值的 getter 函數(shù)抗楔,也可以是 ref:

    // 偵聽一個 getter
    const state = reactive({ count: 0 })
    watch(
      () => state.count,
      (count, prevCount) => {
        /* ... */
      }
    )
    
    // 直接偵聽一個 ref
    const count = ref(0)
    watch(count, (count, prevCount) => {
      /* ... */
    })
    
  • 偵聽多個數(shù)據(jù)源

    watcher 也可以使用數(shù)組來同時偵聽多個源:

    watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
      /* ... */
    })
    
  • watchEffect 共享的行為

    watch 和 watchEffect 在停止偵聽, 清除副作用 (相應(yīng)地 onInvalidate 會作為回調(diào)的第三個參數(shù)傳入),副作用刷新時機偵聽器調(diào)試 等方面行為一致.

  • 類型定義

    // 偵聽單數(shù)據(jù)源
    function watch<T>(
      source: WatcherSource<T>,
      callback: (
        value: T,
        oldValue: T,
        onInvalidate: InvalidateCbRegistrator
      ) => void,
      options?: WatchOptions
    ): StopHandle
    
    // 偵聽多數(shù)據(jù)源
    function watch<T extends WatcherSource<unknown>[]>(
      sources: T
      callback: (
        values: MapSources<T>,
        oldValues: MapSources<T>,
        onInvalidate: InvalidateCbRegistrator
      ) => void,
      options? : WatchOptions
    ): StopHandle
    
    type WatcherSource<T> = Ref<T> | (() => T)
    
    type MapSources<T> = {
      [K in keyof T]: T[K] extends WatcherSource<infer V> ? V : never
    }
    
    // 共有的屬性 請查看 `watchEffect` 的類型定義
    interface WatchOptions extends WatchEffectOptions {
      immediate?: boolean // default: false
      deep?: boolean
    }
    

#生命周期鉤子函數(shù)

可以直接導(dǎo)入 onXXX 一族的函數(shù)來注冊生命周期鉤子:

import { onMounted, onUpdated, onUnmounted } from 'vue'

const MyComponent = {
  setup() {
    onMounted(() => {
      console.log('mounted!')
    })
    onUpdated(() => {
      console.log('updated!')
    })
    onUnmounted(() => {
      console.log('unmounted!')
    })
  },
}

這些生命周期鉤子注冊函數(shù)只能在 setup() 期間同步使用拦坠, 因為它們依賴于內(nèi)部的全局狀態(tài)來定位當(dāng)前組件實例(正在調(diào)用 setup() 的組件實例), 不在當(dāng)前組件下調(diào)用這些函數(shù)會拋出一個錯誤连躏。

組件實例上下文也是在生命周期鉤子同步執(zhí)行期間設(shè)置的,因此贞滨,在卸載組件時入热,在生命周期鉤子內(nèi)部同步創(chuàng)建的偵聽器和計算狀態(tài)也將自動刪除。

  • 與 2.x 版本生命周期相對應(yīng)的組合式 API

    • beforeCreate -> 使用 setup()
    • created -> 使用 setup()
    • beforeMount -> onBeforeMount
    • mounted -> onMounted
    • beforeUpdate -> onBeforeUpdate
    • updated -> onUpdated
    • beforeDestroy -> onBeforeUnmount
    • destroyed -> onUnmounted
    • errorCaptured -> onErrorCaptured
  • 新增的鉤子函數(shù)

    除了和 2.x 生命周期等效項之外晓铆,組合式 API 還提供了以下調(diào)試鉤子函數(shù):

    • onRenderTracked
    • onRenderTriggered

    兩個鉤子函數(shù)都接收一個 DebuggerEvent勺良,與 watchEffect 參數(shù)選項中的 onTrackonTrigger 類似:

    export default {
      onRenderTriggered(e) {
        debugger
        // 檢查哪個依賴性導(dǎo)致組件重新渲染
      },
    }
    

#依賴注入

provideinject 提供依賴注入,功能類似 2.x 的 provide/inject骄噪。兩者都只能在當(dāng)前活動組件實例的 setup() 中調(diào)用尚困。

import { provide, inject } from 'vue'

const ThemeSymbol = Symbol()

const Ancestor = {
  setup() {
    provide(ThemeSymbol, 'dark')
  },
}

const Descendent = {
  setup() {
    const theme = inject(ThemeSymbol, 'light' /* optional default value */)
    return {
      theme,
    }
  },
}

inject 接受一個可選的的默認值作為第二個參數(shù)。如果未提供默認值腰池,并且在 provide 上下文中未找到該屬性尾组,則 inject 返回 undefined忙芒。

  • 注入的響應(yīng)性

    可以使用 ref 來保證 providedinjected 之間值的響應(yīng):

    // 提供者:
    const themeRef = ref('dark')
    provide(ThemeSymbol, themeRef)
    
    // 使用者:
    const theme = inject(ThemeSymbol, ref('light'))
    watchEffect(() => {
      console.log(`theme set to: ${theme.value}`)
    })
    

    如果注入一個響應(yīng)式對象示弓,則它的狀態(tài)變化也可以被偵聽。

  • 類型定義

    interface InjectionKey<T> extends Symbol {}
    
    function provide<T>(key: InjectionKey<T> | string, value: T): void
    
    // 未傳呵萨,使用缺省值
    function inject<T>(key: InjectionKey<T> | string): T | undefined
    // 傳入了默認值
    function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T
    

    Vue 提供了一個繼承 SymbolInjectionKey 接口奏属。它可用于在提供者和消費者之間同步注入值的類型:

    import { InjectionKey, provide, inject } from 'vue'
    
    const key: InjectionKey<string> = Symbol()
    
    provide(key, 'foo') // 類型不是 string 則會報錯
    
    const foo = inject(key) // foo 的類型: string | undefined
    

    如果使用字符串作為鍵或沒有定義類型的符號,則需要顯式聲明注入值的類型:

    const foo = inject<string>('foo') // string | undefined
    

#模板 Refs

當(dāng)使用組合式 API 時潮峦,reactive refstemplate refs 的概念已經(jīng)是統(tǒng)一的囱皿。為了獲得對模板內(nèi)元素或組件實例的引用勇婴,我們可以像往常一樣在 setup() 中聲明一個 ref 并返回它:

<template>
  <div ref="root"></div>
</template>

<script>
  import { ref, onMounted } from 'vue'

  export default {
    setup() {
      const root = ref(null)

      onMounted(() => {
        // 在渲染完成后, 這個 div DOM 會被賦值給 root ref 對象
        console.log(root.value) // <div/>
      })

      return {
        root,
      }
    },
  }
</script>

這里我們將 root 暴露在渲染上下文中,并通過 ref="root" 綁定到 div 作為其 ref嘱腥。 在 Virtual DOM patch 算法中耕渴,如果一個 VNode 的 ref 對應(yīng)一個渲染上下文中的 ref,則該 VNode 對應(yīng)的元素或組件實例將被分配給該 ref齿兔。 這是在 Virtual DOM 的 mount / patch 過程中執(zhí)行的橱脸,因此模板 ref 僅在渲染初始化后才能訪問。

ref 被用在模板中時和其他 ref 一樣:都是響應(yīng)式的分苇,并可以傳遞進組合函數(shù)(或從其中返回)添诉。

  • 配合 render 函數(shù) / JSX 的用法

    export default {
      setup() {
        const root = ref(null)
    
        return () =>
          h('div', {
            ref: root,
          })
    
        // 使用 JSX
        return () => <div ref={root} />
      },
    }
    
  • v-for 中使用

    模板 ref 在 v-for 中使用 vue 沒有做特殊處理,需要使用函數(shù)型的 ref(3.0 提供的新功能)來自定義處理方式:

    <template>
      <div v-for="(item, i) in list" :ref="el => { divs[i] = el }">
        {{ item }}
      </div>
    </template>
    
    <script>
      import { ref, reactive, onBeforeUpdate } from 'vue'
    
      export default {
        setup() {
          const list = reactive([1, 2, 3])
          const divs = ref([])
    
          // 確保在每次變更之前重置引用
          onBeforeUpdate(() => {
            divs.value = []
          })
    
          return {
            list,
            divs,
          }
        },
      }
    </script>
    

#響應(yīng)式系統(tǒng)工具集

#unref

如果參數(shù)是一個 ref 則返回它的 value医寿,否則返回參數(shù)本身栏赴。它是 val = isRef(val) ? val.value : val 的語法糖。

function useFoo(x: number | Ref<number>) {
  const unwrapped = unref(x) // unwrapped 一定是 number 類型
}

#toRef

toRef 可以用來為一個 reactive 對象的屬性創(chuàng)建一個 ref靖秩。這個 ref 可以被傳遞并且能夠保持響應(yīng)性须眷。

const state = reactive({
  foo: 1,
  bar: 2,
})

const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

當(dāng)您要將一個 prop 中的屬性作為 ref 傳給組合邏輯函數(shù)時,toRef 就派上了用場:

export default {
  setup(props) {
    useSomeFeature(toRef(props, 'foo'))
  },
}

#toRefs

把一個響應(yīng)式對象轉(zhuǎn)換成普通對象沟突,該普通對象的每個 property 都是一個 ref 柒爸,和響應(yīng)式對象 property 一一對應(yīng)。

const state = reactive({
  foo: 1,
  bar: 2,
})

const stateAsRefs = toRefs(state)
/*
stateAsRefs 的類型如下:

{
  foo: Ref<number>,
  bar: Ref<number>
}
*/

// ref 對象 與 原屬性的引用是 "鏈接" 上的
state.foo++
console.log(stateAsRefs.foo) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

當(dāng)想要從一個組合邏輯函數(shù)中返回響應(yīng)式對象時事扭,用 toRefs 是很有效的捎稚,該 API 讓消費組件可以 解構(gòu) / 擴展(使用 ... 操作符)返回的對象,并不會丟失響應(yīng)性:

function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2,
  })

  // 對 state 的邏輯操作

  // 返回時將屬性都轉(zhuǎn)為 ref
  return toRefs(state)
}

export default {
  setup() {
    // 可以解構(gòu)求橄,不會丟失響應(yīng)性
    const { foo, bar } = useFeatureX()

    return {
      foo,
      bar,
    }
  },
}

#isRef

檢查一個值是否為一個 ref 對象今野。

#isProxy

檢查一個對象是否是由 reactive 或者 readonly 方法創(chuàng)建的代理。

#isReactive

檢查一個對象是否是由 reactive 創(chuàng)建的響應(yīng)式代理罐农。

如果這個代理是由 readonly 創(chuàng)建的条霜,但是又被 reactive 創(chuàng)建的另一個代理包裹了一層,那么同樣也會返回 true涵亏。

#isReadonly

檢查一個對象是否是由 readonly 創(chuàng)建的只讀代理宰睡。

#高級響應(yīng)式系統(tǒng) API

#customRef

customRef 用于自定義一個 ref,可以顯式地控制依賴追蹤和觸發(fā)響應(yīng)气筋,接受一個工廠函數(shù)拆内,兩個參數(shù)分別是用于追蹤的 track 與用于觸發(fā)響應(yīng)的 trigger,并返回一個一個帶有 getset 屬性的對象

  • 使用自定義 ref 實現(xiàn)帶防抖功能的 v-model

    <input v-model="text" />
    
    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'),
        }
      },
    }
    
  • 類型定義

    function customRef<T>(factory: CustomRefFactory<T>): Ref<T>
    
    type CustomRefFactory<T> = (
      track: () => void,
      trigger: () => void
    ) => {
      get: () => T
      set: (value: T) => void
    }
    

#markRaw

顯式標(biāo)記一個對象為“永遠不會轉(zhuǎn)為響應(yīng)式代理”宠默,函數(shù)返回這個對象本身麸恍。

const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

// 如果被 markRaw 標(biāo)記了,即使在響應(yīng)式對象中作屬性,也依然不是響應(yīng)式的
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false

注意

markRaw 和下面的 shallowXXX 一族的 API 允許你可選擇性的覆蓋 reactive readonly 默認 "深層的" 特性抹沪,或者使用無代理的普通對象刻肄。設(shè)計這種「淺層讀取」有很多原因,比如:

  • 一些值的實際上的用法非常簡單融欧,并沒有必要轉(zhuǎn)為響應(yīng)式敏弃,比如某個復(fù)雜的第三方類庫的實例,或者 Vue 組件對象
  • 當(dāng)渲染一個元素數(shù)量龐大噪馏,但是數(shù)據(jù)是不可變的权她,跳過 Proxy 的轉(zhuǎn)換可以帶來性能提升。

這些 API 被認為是高級的逝薪,是因為這種特性僅停留在根級別隅要,所以如果你將一個嵌套的,沒有 markRaw 的對象設(shè)置為 reactive 對象的屬性董济,在重新訪問時步清,你又會得到一個 Proxy 的版本,在使用中最終會導(dǎo)致標(biāo)識混淆的嚴重問題:執(zhí)行某個操作同時依賴于某個對象的原始版本和代理版本虏肾。

const foo = markRaw({
  nested: {},
})

const bar = reactive({
  // 盡管 `foo` 己經(jīng)被標(biāo)記為 raw 了, 但 foo.nested 并沒有
  nested: foo.nested,
})

console.log(foo.nested === bar.nested) // false

標(biāo)識混淆在一般使用當(dāng)中應(yīng)該是非常罕見的廓啊,但是要想完全避免這樣的問題,必須要對整個響應(yīng)式系統(tǒng)的工作原理有一個相當(dāng)清晰的認知封豪。

#shallowReactive

只為某個對象的私有(第一層)屬性創(chuàng)建淺層的響應(yīng)式代理谴轮,不會對“屬性的屬性”做深層次、遞歸地響應(yīng)式代理吹埠,而只是保留原樣第步。

const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2,
  },
})

// 變更 state 的自有屬性是響應(yīng)式的
state.foo++
// ...但不會深層代理
isReactive(state.nested) // false
state.nested.bar++ // 非響應(yīng)式

#shallowReadonly

只為某個對象的自有(第一層)屬性創(chuàng)建淺層的只讀響應(yīng)式代理,同樣也不會做深層次缘琅、遞歸地代理粘都,深層次的屬性并不是只讀的。

const state = shallowReadonly({
  foo: 1,
  nested: {
    bar: 2,
  },
})

// 變更 state 的自有屬性會失敗
state.foo++
// ...但是嵌套的對象是可以變更的
isReadonly(state.nested) // false
state.nested.bar++ // 嵌套屬性依然可修改

#shallowRef

創(chuàng)建一個 ref 刷袍,將會追蹤它的 .value 更改操作翩隧,但是并不會對變更后的 .value 做響應(yīng)式代理轉(zhuǎn)換(即變更不會調(diào)用 reactive

const foo = shallowRef({})
// 更改對操作會觸發(fā)響應(yīng)
foo.value = {}
// 但上面新賦的這個對象并不會變?yōu)轫憫?yīng)式對象
isReactive(foo.value) // false

#toRaw

返回由 reactivereadonly 方法轉(zhuǎn)換成響應(yīng)式代理的普通對象。這是一個還原方法呻纹,可用于臨時讀取堆生,訪問不會被代理/跟蹤,寫入時也不會觸發(fā)更改雷酪。不建議一直持有原始對象的引用淑仆。請謹慎使用。

const foo = {}
const reactiveFoo = reactive(foo)

console.log(toRaw(reactiveFoo) === foo) // true
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末太闺,一起剝皮案震驚了整個濱河市糯景,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌省骂,老刑警劉巖蟀淮,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異钞澳,居然都是意外死亡怠惶,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門轧粟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來策治,“玉大人,你說我怎么就攤上這事兰吟⊥ū梗” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵混蔼,是天一觀的道長履腋。 經(jīng)常有香客問我,道長惭嚣,這世上最難降的妖魔是什么遵湖? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮晚吞,結(jié)果婚禮上延旧,老公的妹妹穿的比我還像新娘。我一直安慰自己槽地,他們只是感情好迁沫,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著捌蚊,像睡著了一般弯洗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上逢勾,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天牡整,我揣著相機與錄音,去河邊找鬼溺拱。 笑死逃贝,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的迫摔。 我是一名探鬼主播沐扳,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼句占!你這毒婦竟也來了沪摄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎杨拐,沒想到半個月后祈餐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡哄陶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年帆阳,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屋吨。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡蜒谤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出至扰,到底是詐尸還是另有隱情鳍徽,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布敢课,位于F島的核電站阶祭,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏翎猛。R本人自食惡果不足惜胖翰,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望切厘。 院中可真熱鬧萨咳,春花似錦、人聲如沸疫稿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽遗座。三九已至舀凛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間途蒋,已是汗流浹背猛遍。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留号坡,地道東北人懊烤。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像宽堆,于是被迫代替她去往敵國和親腌紧。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容