欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Vue開發(fā)手冊Function-based?API?RFC

 更新時(shí)間:2022年11月07日 11:40:15   作者:尤雨溪  
這篇文章主要為大家介紹了Vue開發(fā)手冊Function-based?API?RFC使用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

概要

2020 年一月又注:RFC 已經(jīng)被完全重寫,最新版本請以 https://composition-api.vuejs.org/ 為準(zhǔn)。以下內(nèi)容會(huì)有部分與最新的 API 有出入,但依然可以幫助理解。

譯注:這是 3.0 最重要的 RFC,因此特意翻譯成中文。

將 2.x 中與組件邏輯相關(guān)的選項(xiàng)以 API 函數(shù)的形式重新設(shè)計(jì)。

基本例子

import { ref, computed, watch, onMounted } from 'vue'
const App = {
  template: `
    <div>
      <span>count is {{ count }}</span>
      <span>plusOne is {{ plusOne }}</span>
      <button @click="increment">count++</button>
    </div>
  `,
  setup() {
    // reactive state
    const count = ref(0)
    // computed state
    const plusOne = computed(() => count.value + 1)
    // method
    const increment = () => { count.value++ }
    // watch
    watch(() => count.value * 2, val => {
      console.log(`count * 2 is ${val}`)
    })
    // lifecycle
    onMounted(() => {
      console.log(`mounted`)
    })
    // expose bindings on render context
    return {
      count,
      plusOne,
      increment
    }
  }
}

設(shè)計(jì)動(dòng)機(jī)

邏輯組合與復(fù)用

組件 API 設(shè)計(jì)所面對的核心問題之一就是如何組織邏輯,以及如何在多個(gè)組件之間抽取和復(fù)用邏輯?;?Vue 2.x 目前的 API 我們有一些常見的邏輯復(fù)用模式,但都或多或少存在一些問題。這些模式包括:

  • Mixins
  • 高階組件 (Higher-order Components, aka HOCs)
  • Renderless Components (基于 scoped slots / 作用域插槽封裝邏輯的組件)

網(wǎng)絡(luò)上關(guān)于這些模式的介紹很多,這里就不再贅述細(xì)節(jié)。總體來說,以上這些模式存在以下問題:

  • 模版中的數(shù)據(jù)來源不清晰。舉例來說,當(dāng)一個(gè)組件中使用了多個(gè) mixin 的時(shí)候,光看模版會(huì)很難分清一個(gè)屬性到底是來自哪一個(gè) mixin。HOC 也有類似的問題。
  • 命名空間沖突。由不同開發(fā)者開發(fā)的 mixin 無法保證不會(huì)正好用到一樣的屬性或是方法名。HOC 在注入的 props 中也存在類似問題。
  • 性能。HOC 和 Renderless Components 都需要額外的組件實(shí)例嵌套來封裝邏輯,導(dǎo)致無謂的性能開銷。

Function-based API 受 React Hooks 的啟發(fā),提供了一個(gè)全新的邏輯復(fù)用方案,且不存在上述問題。使用基于函數(shù)的 API,我們可以將相關(guān)聯(lián)的代碼抽取到一個(gè) "composition function"(組合函數(shù))中 —— 該函數(shù)封裝了相關(guān)聯(lián)的邏輯,并將需要暴露給組件的狀態(tài)以響應(yīng)式的數(shù)據(jù)源的方式返回出來。這里是一個(gè)用組合函數(shù)來封裝鼠標(biāo)位置偵聽邏輯的例子:

function useMouse() {
  const x = ref(0)
  const y = ref(0)
  const update = e => {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return { x, y }
}
// 在組件中使用該函數(shù)
const Component = {
  setup() {
    const { x, y } = useMouse()
    // 與其它函數(shù)配合使用
    const { z } = useOtherLogic()
    return { x, y, z }
  },
  template: `<div>{{ x }} {{ y }} {{ z }}</div>`
}

從以上例子中可以看到:

  • 暴露給模版的屬性來源清晰(從函數(shù)返回);
  • 返回值可以被任意重命名,所以不存在命名空間沖突;
  • 沒有創(chuàng)建額外的組件實(shí)例所帶來的性能損耗。

文末附錄中有與 React Hooks 的一些細(xì)節(jié)對比。

類型推導(dǎo)

3.0 的一個(gè)主要設(shè)計(jì)目標(biāo)是增強(qiáng)對 TypeScript 的支持。原本我們期望通過 Class API 來達(dá)成這個(gè)目標(biāo),但是經(jīng)過討論和原型開發(fā),我們認(rèn)為 Class 并不是解決這個(gè)問題的正確路線,基于 Class 的 API 依然存在類型問題。

基于函數(shù)的 API 天然對類型推導(dǎo)很友好,因?yàn)?TS 對函數(shù)的參數(shù)、返回值和泛型的支持已經(jīng)非常完備。更值得一提的是基于函數(shù)的 API 在使用 TS 或是原生 JS 時(shí)寫出來的代碼幾乎是完全一樣的。下文會(huì)提供新 API 類型推導(dǎo)的更多細(xì)節(jié),此外文末附錄中有關(guān)于 Class API 類型問題的更多細(xì)節(jié)。

打包尺寸

基于函數(shù)的 API 每一個(gè)函數(shù)都可以作為 named ES export 被單獨(dú)引入,這使得它們對 tree-shaking 非常友好。沒有被使用的 API 的相關(guān)代碼可以在最終打包時(shí)被移除。同時(shí),基于函數(shù) API 所寫的代碼也有更好的壓縮效率,因?yàn)樗械暮瘮?shù)名和 setup 函數(shù)體內(nèi)部的變量名都可以被壓縮,但對象和 class 的屬性/方法名卻不可以。

設(shè)計(jì)細(xì)節(jié)

setup() 函數(shù)

我們將會(huì)引入一個(gè)新的組件選項(xiàng),setup()。顧名思義,這個(gè)函數(shù)將會(huì)是我們 setup 我們組件邏輯的地方,它會(huì)在一個(gè)組件實(shí)例被創(chuàng)建時(shí),初始化了 props 之后調(diào)用。setup() 會(huì)接收到初始的 props 作為參數(shù):

const MyComponent = {
  props: {
    name: String
  },
  setup(props) {
    console.log(props.name)
  }
}

需要留意的是這里傳進(jìn)來的 props 對象是響應(yīng)式的 —— 它可以被當(dāng)作數(shù)據(jù)源去觀測,當(dāng)后續(xù) props 發(fā)生變動(dòng)時(shí)它也會(huì)被框架內(nèi)部同步更新。但對于用戶代碼來說,它是不可修改的(會(huì)導(dǎo)致警告)。

在 setup 內(nèi)部可以使用 this,但你大部分時(shí)候不會(huì)需要它。

組件狀態(tài)

類似 data(),setup() 可以返回一個(gè)對象 —— 這個(gè)對象上的屬性將會(huì)被暴露給模版的渲染上下文:

const MyComponent = {
  props: {
    name: String
  },
  setup(props) {
    return {
      msg: `hello ${props.name}!`
    }
  },
  template: `<div>{{ msg }}</div>`
}

上面這個(gè)例子跟 data() 一模一樣:msg 可以在模版中被直接使用,它甚至可以被模版中的內(nèi)聯(lián)函數(shù)修改。但如果我們想要?jiǎng)?chuàng)建一個(gè)可以在 setup() 內(nèi)部被管理的值,可以使用 ref 函數(shù):

import { ref } from 'vue'
const MyComponent = {
  setup(props) {
    const msg = ref('hello')
    const appendName = () => {
      msg.value = `hello ${props.name}`
    }
    return {
      msg,
      appendName
    }
  },
  template: `<div @click="appendName">{{ msg }}</div>`
}

ref() 返回的是一個(gè) value reference (包裝對象)。一個(gè)包裝對象只有一個(gè)屬性:.value ,該屬性指向內(nèi)部被包裝的值。在上面的例子中,msg 包裝的是一個(gè)字符串。包裝對象的值可以被直接修改:

// 讀取
console.log(msg.value) // 'hello'
// 修改
msg.value = 'bye'

為什么需要包裝對象?

我們知道在 JavaScript 中,原始值類型如 string 和 number 是只有值,沒有引用的。如果在一個(gè)函數(shù)中返回一個(gè)字符串變量,接收到這個(gè)字符串的代碼只會(huì)獲得一個(gè)值,是無法追蹤原始變量后續(xù)的變化的。

因此,包裝對象的意義就在于提供一個(gè)讓我們能夠在函數(shù)之間以引用的方式傳遞任意類型值的容器。這有點(diǎn)像 React Hooks 中的 useRef —— 但不同的是 Vue 的包裝對象同時(shí)還是響應(yīng)式的數(shù)據(jù)源。有了這樣的容器,我們就可以在封裝了邏輯的組合函數(shù)中將狀態(tài)以引用的方式傳回給組件。組件負(fù)責(zé)展示(追蹤依賴),組合函數(shù)負(fù)責(zé)管理狀態(tài)(觸發(fā)更新):

setup() {
  const valueA = useLogicA() // valueA 可能被 useLogicA() 內(nèi)部的代碼修改從而觸發(fā)更新
  const valueB = useLogicB()
  return {
    valueA,
    valueB
  }
}

包裝對象也可以包裝非原始值類型的數(shù)據(jù),被包裝的對象中嵌套的屬性都會(huì)被響應(yīng)式地追蹤。用包裝對象去包裝對象或是數(shù)組并不是沒有意義的:它讓我們可以對整個(gè)對象的值進(jìn)行替換 —— 比如用一個(gè) filter 過的數(shù)組去替代原數(shù)組:

const numbers = ref([1, 2, 3])
// 替代原數(shù)組,但引用不變
numbers.value = numbers.value.filter(n => n > 1)

如果你依然想創(chuàng)建一個(gè)沒有包裝的響應(yīng)式對象,可以使用 reactiveAPI(和 2.x 的 Vue.observable()等同):

import { reactive } from 'vue'
const object = reactive({
  count: 0
})
object.count++

Ref Unwrapping(包裝對象的自動(dòng)展開)

在上面的一個(gè)例子中你可能注意到了,雖然 setup()返回的 msg是一個(gè)包裝對象,但在模版中我們直接用了 {{ msg }}這樣的綁定,沒有用 .value。這是因?yàn)楫?dāng)包裝對象被暴露給模版渲染上下文,或是被嵌套在另一個(gè)響應(yīng)式對象中的時(shí)候,它會(huì)被自動(dòng)展開 (unwrap) 為內(nèi)部的值。

比如一個(gè)包裝對象的綁定可以直接被模版中的內(nèi)聯(lián)函數(shù)修改:

const MyComponent = {
  setup() {
    return {
      count: ref(0)
    }
  },
  template: `<button @click="count++">{{ count }}</button>`
}

當(dāng)一個(gè)包裝對象被作為另一個(gè)響應(yīng)式對象的屬性引用的時(shí)候也會(huì)被自動(dòng)展開:

const count = ref(0)
const obj = reactive({
  count
})
console.log(obj.count) // 0
obj.count++
console.log(obj.count) // 1
console.log(count.value) // 1
count.value++
console.log(obj.count) // 2
console.log(count.value) // 2

以上這些關(guān)于包裝對象的細(xì)節(jié)可能會(huì)讓你覺得有些復(fù)雜,但實(shí)際使用中你只需要記住一個(gè)基本的規(guī)則:只有當(dāng)你直接以變量的形式引用一個(gè)包裝對象的時(shí)候才會(huì)需要用 .value 去取它內(nèi)部的值 —— 在模版中你甚至不需要知道它們的存在。

配合手寫 Render 函數(shù)使用

如果你的組件不使用模版,你也可以選擇在 setup() 中直接返回一個(gè)渲染函數(shù):

import { ref, createElement as h } from 'vue'
const MyComponent = {
  setup(initialProps) {
    const count = ref(0)
    const increment = () => { count.value++ }
    return (props, slots, attrs, vnode) => (
      h('button', {
        onClick: increment
      }, count.value)
    )
  }
} 

返回的函數(shù)應(yīng)當(dāng)遵循 RFC#28 中提出的函數(shù)簽名。你可能注意到了 setup() 和其返回的渲染函數(shù)的第一個(gè)參數(shù)都是 props —— 它們的行為是一樣的,但是渲染函數(shù)接收到的 props 在生產(chǎn)模式下將會(huì)是一個(gè)普通對象,因此它的性能會(huì)更好些。

和 2.x 一樣的 render 選項(xiàng)也可以使用,但如果用了 setup(),就應(yīng)該盡量使用內(nèi)聯(lián)返回的渲染函數(shù),因?yàn)檫@樣可以避免先返回一堆綁定然后再在另一個(gè)函數(shù)里解構(gòu)出來,同時(shí)類型推導(dǎo)也會(huì)更簡單直接一些。

Computed Value (計(jì)算值)

除了直接包裝一個(gè)可變的值,我們也可以包裝通過計(jì)算產(chǎn)生的值:

import { ref, computed } from 'vue'
const count = ref(0)
const countPlusOne = computed(() => count.value + 1)
console.log(countPlusOne.value) // 1
count.value++
console.log(countPlusOne.value) // 2

計(jì)算值的行為跟計(jì)算屬性 (computed property) 一樣:只有當(dāng)依賴變化的時(shí)候它才會(huì)被重新計(jì)算。

computed() 返回的是一個(gè)只讀的包裝對象,它可以和普通的包裝對象一樣在 setup() 中被返回 ,也一樣會(huì)在渲染上下文中被自動(dòng)展開。默認(rèn)情況下,如果用戶試圖去修改一個(gè)只讀包裝對象,會(huì)觸發(fā)警告。

雙向計(jì)算值可以通過傳給 computed 第二個(gè)參數(shù)作為 setter 來創(chuàng)建:

const count = value(0)
const writableComputed = computed(
  // read
  () => count.value + 1,
  // write
  val => {
    count.value = val - 1
  }
)

Watchers

watch() API 提供了基于觀察狀態(tài)的變化來執(zhí)行副作用的能力。

watch() 接收的第一個(gè)參數(shù)被稱作 “數(shù)據(jù)源”,它可以是:

  • 一個(gè)返回任意值的函數(shù)
  • 一個(gè)包裝對象
  • 一個(gè)包含上述兩種數(shù)據(jù)源的數(shù)組

第二個(gè)參數(shù)是回調(diào)函數(shù)。回調(diào)函數(shù)只有當(dāng)數(shù)據(jù)源發(fā)生變動(dòng)時(shí)才會(huì)被觸發(fā):

watch(
  // getter
  () => count.value + 1,
  // callback
  (value, oldValue) => {
    console.log('count + 1 is: ', value)
  }
)
// -> count + 1 is: 1
count.value++
// -> count + 1 is: 2

和 2.x 的 $watch 有所不同的是,watch() 的回調(diào)會(huì)在創(chuàng)建時(shí)就執(zhí)行一次。這有點(diǎn)類似 2.x watcher 的 immediate: true 選項(xiàng),但有一個(gè)重要的不同:默認(rèn)情況下 watch() 的回調(diào)總是會(huì)在當(dāng)前的 renderer flush 之后才被調(diào)用 —— 換句話說,watch()的回調(diào)在觸發(fā)時(shí),DOM 總是會(huì)在一個(gè)已經(jīng)被更新過的狀態(tài)下。 這個(gè)行為是可以通過選項(xiàng)來定制的。

在 2.x 的代碼中,我們經(jīng)常會(huì)遇到同一份邏輯需要在 mounted 和一個(gè) watcher 的回調(diào)中執(zhí)行(比如根據(jù)當(dāng)前的 id 抓取數(shù)據(jù)),3.0 的 watch() 默認(rèn)行為可以直接表達(dá)這樣的需求。

觀察 props

上面提到了 setup() 接收到的 props 對象是一個(gè)可觀測的響應(yīng)式對象:

const MyComponent = {
  props: {
    id: Number
  },
  setup(props) {
    const data = ref(null)
    watch(() => props.id, async (id) => {
      data.value = await fetchData(id)
    })
    return {
      data
    }
  }
}

觀察包裝對象

watch()可以直接觀察一個(gè)包裝對象:

// double 是一個(gè)計(jì)算包裝對象
const double = computed(() => count.value * 2)
watch(double, value => {
  console.log('double the count is: ', value)
}) // -> double the count is: 0
count.value++ // -> double the count is: 2

觀察多個(gè)數(shù)據(jù)源

watch() 也可以觀察一個(gè)包含多個(gè)數(shù)據(jù)源的數(shù)組 - 這種情況下,任意一個(gè)數(shù)據(jù)源的變化都會(huì)觸發(fā)回調(diào),同時(shí)回調(diào)會(huì)接收到包含對應(yīng)值的數(shù)組作為參數(shù):

watch(
  [refA, () => refB.value],
  ([a, b], [prevA, prevB]) => {
    console.log(`a is: ${a}`)
    console.log(`b is: $`)
  }
)

停止觀察

watch() 返回一個(gè)停止觀察的函數(shù):

const stop = watch(...)
// stop watching
stop()

如果 watch() 是在一個(gè)組件的 setup() 或是生命周期函數(shù)中被調(diào)用的,那么該 watcher 會(huì)在當(dāng)前組件被銷毀時(shí)也一同被自動(dòng)停止:

export default {
  setup() {
    // 組件銷毀時(shí)也會(huì)被自動(dòng)停止
    watch(/* ... */)
  }
}

清理副作用

有時(shí)候當(dāng)觀察的數(shù)據(jù)源變化后,我們可能需要對之前所執(zhí)行的副作用進(jìn)行清理。舉例來說,一個(gè)異步操作在完成之前數(shù)據(jù)就產(chǎn)生了變化,我們可能要撤銷還在等待的前一個(gè)操作。為了處理這種情況,watcher 的回調(diào)會(huì)接收到的第三個(gè)參數(shù)是一個(gè)用來注冊清理操作的函數(shù)。調(diào)用這個(gè)函數(shù)可以注冊一個(gè)清理函數(shù)。清理函數(shù)會(huì)在下屬情況下被調(diào)用:

  • 在回調(diào)被下一次調(diào)用前
  • 在 watcher 被停止前
watch(idValue, (id, oldId, onCleanup) => {
  const token = performAsyncOperation(id)
  onCleanup(() => {
    // id 發(fā)生了變化,或是 watcher 即將被停止.
    // 取消還未完成的異步操作。
    token.cancel()
  })
})

之所以要用傳入的注冊函數(shù)來注冊清理函數(shù),而不是像 React 的 useEffect 那樣直接返回一個(gè)清理函數(shù),是因?yàn)?watcher 回調(diào)的返回值在異步場景下有特殊作用。我們經(jīng)常需要在 watcher 的回調(diào)中用 async function 來執(zhí)行異步操作:

const data = ref(null)
watch(getId, async (id) => {
  data.value = await fetchData(id)
})

我們知道 async function 隱性地返回一個(gè) Promise - 這樣的情況下,我們是無法返回一個(gè)需要被立刻注冊的清理函數(shù)的。除此之外,回調(diào)返回的 Promise 還會(huì)被 Vue 用于內(nèi)部的異步錯(cuò)誤處理。

Watcher 回調(diào)的調(diào)用時(shí)機(jī)

默認(rèn)情況下,所有的 watcher 回調(diào)都會(huì)在當(dāng)前的 renderer flush 之后被調(diào)用。這確保了在回調(diào)中 DOM 永遠(yuǎn)都已經(jīng)被更新完畢。如果你想要讓回調(diào)在 DOM 更新之前或是被同步觸發(fā),可以使用 flush 選項(xiàng):

watch(
  () => count.value + 1,
  () => console.log(`count changed`),
  {
    flush: 'post', // default, fire after renderer flush
    flush: 'pre', // fire right before renderer flush
    flush: 'sync' // fire synchronously
  }
)

全部的 watch 選項(xiàng)(TS 類型聲明)

interface WatchOptions {
  lazy?: boolean
  deep?: boolean
  flush?: 'pre' | 'post' | 'sync'
  onTrack?: (e: DebuggerEvent) => void
  onTrigger?: (e: DebuggerEvent) => void
}
interface DebuggerEvent {
  effect: ReactiveEffect
  target: any
  key: string | symbol | undefined
  type: 'set' | 'add' | 'delete' | 'clear' | 'get' | 'has' | 'iterate'
}
  • lazy與 2.x 的 immediate 正好相反
  • deep與 2.x 行為一致
  • onTrack 和 onTrigger 是兩個(gè)用于 debug 的鉤子,分別在 watcher 追蹤到依賴和依賴發(fā)生變化的時(shí)候被調(diào)用,獲得的參數(shù)是一個(gè)包含了依賴細(xì)節(jié)的 debugger event。

生命周期函數(shù)

所有現(xiàn)有的生命周期鉤子都會(huì)有對應(yīng)的 onXXX 函數(shù)(只能在 setup() 中使用):

import { onMounted, onUpdated, onUnmounted } from 'vue'
const MyComponent = {
  setup() {
    onMounted(() => {
      console.log('mounted!')
    })
    onUpdated(() => {
      console.log('updated!')
    })
    // destroyed 調(diào)整為 unmounted
    onUnmounted(() => {
      console.log('unmounted!')
    })
  }
}

依賴注入

import { provide, inject } from 'vue'
const CountSymbol = Symbol()
const Ancestor = {
  setup() {
    // providing a ref can make it reactive
    const count = ref(0)
    provide(CountSymbol, count)
  }
}
const Descendent = {
  setup() {
    const count = inject(CountSymbol)
    return {
      count
    }
  }
}

如果注入的是一個(gè)包裝對象,則該注入綁定會(huì)是響應(yīng)式的(也就是說,如果 Ancestor 修改了 count,會(huì)觸發(fā) Descendent 的更新)。

類型推導(dǎo)

為了能夠在 TypeScript 中提供正確的類型推導(dǎo),我們需要通過一個(gè)函數(shù)來定義組件:

import { defineComponent, ref } from 'vue'
const MyComponent = defineComponent({
  // props declarations are used to infer prop types
  props: {
    msg: String
  },
  setup(props) {
    props.msg // string | undefined
    // bindings returned from setup() can be used for type inference
    // in templates
    const count = ref(0)
    return {
      count
    }
  }
})

defineComponent 從概念上來說和 2.x 的 Vue.extend 是一樣的,但在 3.0 中它其實(shí)是單純?yōu)榱祟愋屯茖?dǎo)而存在的,內(nèi)部實(shí)現(xiàn)是個(gè) noop(直接返回參數(shù)本身)。它的返回類型可以用于 TSX 和 Vetur 的模版自動(dòng)補(bǔ)全。如果你使用單文件組件,則 Vetur 可以自動(dòng)隱式地幫你添加這個(gè)調(diào)用。

如果你使用手寫 render 函數(shù)或是 TSX,那么你可以在 setup() 當(dāng)中直接返回一個(gè)渲染函數(shù)(注意這里不需要任何手動(dòng)的類型聲明):

import { defineComponent, ref, h } from 'vue'
const MyComponent = defineComponent({
  props: {
    msg: String
  },
  setup(props) {
    const count = ref(0)
    return () => h('div', [
      h('p', `msg is ${props.msg}`),
      h('p', `count is ${count.value}`)
    ])
  }
}) 

純 TypeScript 的 Props 類型聲明

3.0 的 props 選項(xiàng)不是必須的,如果你不需要運(yùn)行時(shí)的 props 類型檢查,你也可以選擇完全在 TypeScript 的類型層面聲明 props 的類型:

import { defineComponent, h } from 'vue'
interface Props {
  msg: string
}
const MyComponent = defineComponent({
  setup(props: Props) {
    return () => h('div', props.msg)
  }
})

如果不需要除了 setup 之外的選項(xiàng),甚至可以直接傳一個(gè)函數(shù)給 defineComponent

const MyComponent = createComponent((props: { msg: string }) => {
  return () => h('div', props.msg)
}) 

這里返回的 MyComponent 也可以在 TSX 中提供正確的 props 補(bǔ)全和推導(dǎo)。

Required Props

Props 默認(rèn)都是可選的,也就是說它們的類型都可能是 undefined。非可選的 props 需要聲明 required: true :

import { defineComponent } from 'vue'
defineComponent({
  props: {
    foo: {
      type: String,
      required: true
    },
    bar: {
      type: String
    }
  },
  setup(props) {
    props.foo // string
    props.bar // string | undefined
  }
})

復(fù)雜 Props 類型

Vue 提供的 PropType 類型可以用來聲明任意復(fù)雜度的 props 類型,但需要用 as any 進(jìn)行一次強(qiáng)制類型轉(zhuǎn)換:

import { defineComponent, PropType } from 'vue'
defineComponent({
  props: {
    options: (null as any) as PropType<{ msg: string }>
  },
  setup(props) {
    props.options // { msg: string } | undefined
  }
}) 

依賴注入類型

依賴注入的 inject 方法是唯一必須手動(dòng)聲明類型的 API:

import { defineComponent, inject, Ref } from 'vue'
defineComponent({
  setup() {
    const count: Ref<number> = inject(CountSymbol)
    return {
      count
    }
  }
})

這里的 Ref 類型即是包裝對象的類型 ,通過泛型參數(shù)來聲明其內(nèi)部包裝的值的類型。

缺點(diǎn)/潛在問題

新的 API 使得動(dòng)態(tài)地檢視/修改一個(gè)組件的選項(xiàng)變得更困難(原來是一個(gè)對象,現(xiàn)在是一段無法被檢視的函數(shù)體)。

這可能是一件好事,因?yàn)橥ǔT谟脩舸a中動(dòng)態(tài)地檢視/修改組件是一類比較危險(xiǎn)的操作,對于運(yùn)行時(shí)也增加了許多潛在的邊緣情況(特別是組件繼承和使用 mixin 的情況下)。新 API 的靈活性應(yīng)該在絕大部分情況下都可以用更顯式的代碼達(dá)成同樣的結(jié)果。

缺乏經(jīng)驗(yàn)的用戶可能會(huì)寫出 “面條代碼”,因?yàn)樾?API 不像舊 API 那樣強(qiáng)制將組件代碼基于選項(xiàng)切分開來。

我們在 Class API RFC 和內(nèi)部討論中聽到過好幾次這樣的聲音,但我認(rèn)為這是一種沒有必要的擔(dān)憂。雖然理論上新的 API 確實(shí)制約更少,但我認(rèn)為 “面條代碼” 的情況不太可能發(fā)生,這里詳細(xì)解釋一下。

基于函數(shù)的新 API 和基于選項(xiàng)的舊 API 之間的最大區(qū)別,就是新 API 讓抽取邏輯變得非常簡單 —— 就跟在普通的代碼中抽取函數(shù)一樣。也就是說,我們不必只在需要復(fù)用邏輯的時(shí)候才抽取函數(shù),也可以單純?yōu)榱烁玫亟M織代碼去抽取函數(shù)。

基于選項(xiàng)的代碼只是看上去更整潔。一個(gè)復(fù)雜的組件往往需要同時(shí)處理多個(gè)不同的邏輯任務(wù),每個(gè)邏輯任務(wù)所涉及的代碼在選項(xiàng) API 下是被分散在多個(gè)選項(xiàng)之中的。舉例來說,從服務(wù)端抓取一份數(shù)據(jù),可能需要用到 propsdata()mounted 和 watch。極端情況下,如果我們把一個(gè)應(yīng)用中所有的邏輯任務(wù)都放在一個(gè)組件里,這個(gè)組件必然會(huì)變得龐大而難以維護(hù),因?yàn)槊總€(gè)邏輯任務(wù)的代碼都被選項(xiàng)切成了多個(gè)碎片分散在各處。

對比之下,基于函數(shù)的 API 讓我們可以把每個(gè)邏輯任務(wù)的代碼都整理到一個(gè)對應(yīng)的函數(shù)中。當(dāng)我們發(fā)現(xiàn)一個(gè)組件變得過大時(shí),我們會(huì)將它切分成多個(gè)更小的組件;同樣地,如果一個(gè)組件的 setup() 函數(shù)變得很復(fù)雜,我們可以將它切分成多個(gè)更小的函數(shù)。而如果是基于選項(xiàng),則無法做到這樣的切分,因?yàn)橛?mixin 只會(huì)讓事情變得更糟糕。

從這個(gè)角度看,基于選項(xiàng) vs. 基于函數(shù)就好像基于 HTML/CSS/JS 組織代碼 vs. 基于單文件組件來組織代碼。

升級策略

新的 API 和 2.x 的 API 完全兼容(只是多了一個(gè) setup()選項(xiàng)) ,并且可以一起使用。新 API 適合在組件復(fù)雜度明顯的情況下用來更好的組織和復(fù)用邏輯,或是用來提供可高度復(fù)用的插件。

理論上,新 API 可以完全提供 2.x API 的全部能力,因此我們可能會(huì)提供一個(gè)可選的編譯時(shí)選項(xiàng),啟用后可以去掉所有僅為 2.x API 支持而存在的代碼,減少一部分體積和性能開銷。

附錄

與 React Hooks 的對比

這里提出的 API 和 React Hooks 有一定的相似性,具有同等的基于函數(shù)抽取和復(fù)用邏輯的能力,但也有很本質(zhì)的區(qū)別。React Hooks 在每次組件渲染時(shí)都會(huì)調(diào)用,通過隱式地將狀態(tài)掛載在當(dāng)前的內(nèi)部組件節(jié)點(diǎn)上,在下一次渲染時(shí)根據(jù)調(diào)用順序取出。而 Vue 的 setup() 每個(gè)組件實(shí)例只會(huì)在初始化時(shí)調(diào)用一次 ,狀態(tài)通過引用儲(chǔ)存在 setup() 的閉包內(nèi)。這意味著基于 Vue 的函數(shù) API 的代碼:

  • 整體上更符合 JavaScript 的直覺;
  • 不受調(diào)用順序的限制,可以有條件地被調(diào)用;
  • 不會(huì)在后續(xù)更新時(shí)不斷產(chǎn)生大量的內(nèi)聯(lián)函數(shù)而影響引擎優(yōu)化或是導(dǎo)致 GC 壓力;
  • 不需要總是使用 useCallback 來緩存?zhèn)鹘o子組件的回調(diào)以防止過度更新;
  • 不需要擔(dān)心傳了錯(cuò)誤的依賴數(shù)組給 useEffect/useMemo/useCallback 從而導(dǎo)致回調(diào)中使用了過期的值 —— Vue 的依賴追蹤是全自動(dòng)的。

注:React Hooks 的開創(chuàng)性毋庸置疑,也是本提案的靈感來源。Hooks 代碼和 JSX 并置使得對值的使用更簡潔也是其優(yōu)點(diǎn),但其設(shè)計(jì)確實(shí)存在上述問題,而 Vue 的響應(yīng)式系統(tǒng)恰巧能夠讓我們繞過這些問題。

Class API 的類型問題

Class API 提案的主要目的是尋找一個(gè)能夠提供更好的 TypeScript 支持的組件聲明方式。但是由于 Vue 需要將來自多個(gè)選項(xiàng)的屬性混合到同一個(gè)渲染上下文上,這使得即使用了 Class,要得到良好的類型推導(dǎo)也不是很容易。

以 props 的類型推導(dǎo)為例。要將 props 的類型 merge 到 class 的 this 上,我們有兩個(gè)選擇:用 class 的泛型參數(shù),或是用 decorator。

這是用泛型參數(shù)的例子:

interface Props {
  message: string
}
class App extends Component<Props> {
  static props = {
    message: String
  }
} 

由于泛型參數(shù)是純類型層面的,所以我們還需要額外地進(jìn)行一次運(yùn)行時(shí)的 props 選項(xiàng)聲明來獲得正確的行為。這就導(dǎo)致需要進(jìn)行雙重聲明。

使用 decorator 的例子如下:

class App extends Component<Props> {
  @prop message: string
}

Decorators 存在如下問題:

  • ES 的 decorator 提案仍然在 stage-2 且極其不穩(wěn)定。過去一年內(nèi)已經(jīng)經(jīng)歷了兩次徹底大改,且和 TS 現(xiàn)有的實(shí)現(xiàn)已經(jīng)完全脫節(jié)?,F(xiàn)在引入一個(gè)基于 TS decorator 實(shí)現(xiàn)的 API 風(fēng)險(xiǎn)太大。
  • Decorator 只能聲明 class this 上的屬性,卻無法將某一類 decorator 聲明的屬性歸并到一個(gè)對象上(比如 $props),這就導(dǎo)致 this.$props 無法被推導(dǎo),且影響 TSX 的使用。
  • 用戶很可能會(huì)覺得可以用 @prop message: string = 'foo'這樣的寫法去聲明默認(rèn)值,但事實(shí)上技術(shù)層面無法做到符合語義的實(shí)現(xiàn)。

最后,class 還有一個(gè)問題,那就是目前 class method 不支持參數(shù)的 contextual typing,也就是說我們無法基于 class 本身的 fields 來推導(dǎo)某個(gè) method 的參數(shù)類型,需要用戶自己去聲明。

以上就是Vue開發(fā)手冊Function-based API RFC的詳細(xì)內(nèi)容,更多關(guān)于Vue Function-based API RFC的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Vue狀態(tài)管理工具Pinia的安裝與使用教程

    Vue狀態(tài)管理工具Pinia的安裝與使用教程

    這篇文章主要介紹了Vue狀態(tài)管理工具Pinia的安裝與使用,一步一步學(xué)習(xí)如何將pinia運(yùn)用到項(xiàng)目實(shí)戰(zhàn)中去,文中有詳細(xì)的安裝教程和使用方法,并通過代碼示例講解的非常詳細(xì),需要的朋友可以參考下
    2024-03-03
  • vue實(shí)現(xiàn)購物車列表

    vue實(shí)現(xiàn)購物車列表

    這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)購物車列表,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-06-06
  • Element控件Tree實(shí)現(xiàn)數(shù)據(jù)樹形結(jié)構(gòu)的示例代碼

    Element控件Tree實(shí)現(xiàn)數(shù)據(jù)樹形結(jié)構(gòu)的示例代碼

    我們在開發(fā)中肯定會(huì)遇到用樹形展示數(shù)據(jù)的需求,本文主要介紹了Element控件Tree實(shí)現(xiàn)數(shù)據(jù)樹形結(jié)構(gòu)的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-08-08
  • 關(guān)于Vue-cli3煩人的eslint問題

    關(guān)于Vue-cli3煩人的eslint問題

    這篇文章主要介紹了關(guān)于Vue-cli3煩人的eslint問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-09-09
  • Vue 列表上下過渡效果的實(shí)例代碼

    Vue 列表上下過渡效果的實(shí)例代碼

    最近有個(gè)需求,一個(gè)列表上下移動(dòng)要有簡單過渡效果。本文通過實(shí)例代碼給大家介紹Vue 列表上下過渡效果,需要的朋友可以參考下
    2019-06-06
  • Vue+Element樹形表格實(shí)現(xiàn)拖拽排序示例

    Vue+Element樹形表格實(shí)現(xiàn)拖拽排序示例

    本文主要介紹了Vue+Element樹形表格實(shí)現(xiàn)拖拽排序示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-08-08
  • 詳解vue如何使用rules對表單字段進(jìn)行校驗(yàn)

    詳解vue如何使用rules對表單字段進(jìn)行校驗(yàn)

    這篇文章主要介紹了詳解vue如何使用rules對表單字段進(jìn)行校驗(yàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-10-10
  • Vue3?Reactive響應(yīng)式原理邏輯詳解

    Vue3?Reactive響應(yīng)式原理邏輯詳解

    這篇文章主要介紹了Vue3?Reactive響應(yīng)式原理邏輯詳解,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下
    2022-07-07
  • 使用vue3+ts打開echarts的正確方式

    使用vue3+ts打開echarts的正確方式

    這篇文章主要給大家介紹了關(guān)于使用vue3+ts打開echarts的正確方式,在Vue3中使用ECharts組件可以方便地創(chuàng)建各種數(shù)據(jù)可視化圖表,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2023-12-12
  • vue3中vite的@路徑別名與path中resolve實(shí)例詳解

    vue3中vite的@路徑別名與path中resolve實(shí)例詳解

    這篇文章主要給大家介紹了關(guān)于vue3中vite的@路徑別名與path中resolve的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用vue具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2023-02-02

最新評論