Vue 3: Data down, Events up | Vue Mastery - Thorsten Lünborg
前言
在這篇文章中,我將展示一個我們可以應用 Vue 3 的新 Composition API 解決一個特定挑戰的模式。我不會介紹所有的基礎知識,因此熟悉這個 新 API 的基礎知識 將對您有所幫助。
重要提示: Composition API 是一個新增劑,是一個新特性,它沒有也不會取代 Vue 1 和 2 中你所瞭解和喜愛的很好的舊 “Options API”。只要把這個新的 API 看作你工具箱中另一個工具,那麼在使用 Options API 解決有些笨拙的情況下,它可能會派上用場。
我喜歡 Vue 的新 Composition API。對我來說,Vue 的反應系統似乎已經擺脫了元件的約束,現在我可以使用它來編寫任何我想要的反應程式碼。
根據我的經驗,一旦您對它有所瞭解,它就是一個建立靈活、可重用程式碼的極好的方法,這些程式碼可以很好地組合,並讓你看到元件的所有部分和特性是如何互動的。
我將以一個小工具開始這篇文章,你可以用它更容易地使用元件和 v-model
。
概述: v-model
指令
如果你使用過 Vue,你就知道 v-model
指令:
<input type="text" v-model="localStateProperty">
這是一個非常讚的快捷方式,以避免我們輸入複雜的模板標記,像這樣:
<input
type="text"
:value="localStateProperty"
@change="localStateProperty = $event.target.value"
>
最棒的是,我們還可以在元件上使用它:
<message-editor v-model="message">
這相當於做以下事情:
<message-editor
:modelValue="message"
@update:modelValue="message = $event"
>
但是,為了實現屬性(prop)和事件(event)的約定,我們的 <message-editor>
元件必須看起來像這樣:
<template>
<label>
<input type="text" :value="modelValue", @change="(event) => $emit('update:modelValue', event.target.value)" >
<label>
</template>
<script> export default {
props: {
'modelValue': String,
}
}
</script>
然而,這似乎相當冗長。?
我們必須這樣做,因為無法直接寫到屬性。我們必須發出正確的事件,並將其留給父元件來決定如何處理我們傳遞的更新,因為它是父元件的資料,而不是 <message-editor>
元件的資料。因此,在這種情況下,我們不能在 input 上使用 v-model
。煩人。
你可能從 Vue 2 瞭解到 Options API 中有一些處理這個問題的模式,但是今天我想看看使用 Composition API 提供的工具如何以一種簡潔的方式解決這個問題。
挑戰: 減少樣板
我們想要實現的是一個抽象,它允許我們在輸入中使用相同的 v-model
快捷方式,即使我們實際上不想寫入本地狀態,而是想發出正確的事件。這是我們希望模板完成後的樣子:
<template>
<label>
<input
type="text"
v-model="message"
/>
<label>
</template>
那麼,讓我們使用 Composition API 來實現這個:
import { computed } from 'vue'
export default {
props: {
'modelValue': String,
},
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
return {
message,
}
}
}
好吧,這是一段新的程式碼,也許有點異形。讓我們來分解一下:
import { computed } from 'vue'
首先,我們匯入 computed() 函式,該函式返回一個用於計算屬性的引用(ref) —— 一個用於從其他反應性資料(例如 props)派生出值的包裝器。
const message = computed({
get: () => props.modelValue,
set: (event) => emit('update:modelValue')
})
在 setup 中,我們建立了這樣一個計算屬性,但是是一個特殊的屬性:我們計算屬性有一個 getter
和 setter
,因此我們實際上可以讀取它的派生值併為它賦一個新值。
這是我們計算屬性在 Javascript 中使用時的行為:
message.value
// => '這返回一個字串'
message.value = 'This will be emitted up'
// => 呼叫 emit('onUpdate:ModelValue', 'This will be fired up')
透過從 setup() 函式返回這個計算屬性,我們將它暴露給模板。現在,我們可以將它和 v-model 一起使用,得到一個乾淨漂亮的模板:
<template>
<label>
<input
type="text"
v-model="message"
>
<label>
</template>
<script>
import { computed } from 'vue'
export default {
props: {
'modelValue': String,
},
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
return {
message,
}
}
}
</script>
現在模板非常乾淨。但與此同時,我們必須在 setup() 中編寫一堆樣板檔案來實現這一點。看起來我們只是將樣板檔案從模板移到了 setup 函式中。
因此,讓我們將這個邏輯提取到它自己的函式中——一個 composition 函式——或者簡稱為 “composable”。
將其轉換為 composable
composable 只是一個函式,我們使用它從 setup 函式中抽象出一些程式碼。可組合性(Composables)是這個新 Composition API 的優勢,也是允許更好的程式碼抽象和組合的核心原則。
這就是我們的目標:
? modelWrapper.js
import { computed } from 'vue'
export function useModelWrapper(props) {
/* 暫時不討論實現 */
}
? MessageEditor.vue
import { useModelWrapper } from '../utils/modelWrapper'
export default {
props: {
'modelValue': String,
}
setup(props, { emit }) {
return {
message: useModelWrapper(props),
}
}
}
注意,我們的 setup()
函式中的程式碼是如何簡化為一行程式碼的(如果我們假設我們在一個很好的編輯器中工作,它可以為我們自動新增 useModelWrapper
的匯入,比如 VSCode)。
我們是怎麼做到的呢?實際上,我們所要做的就是將 setup
中的程式碼複製貼上到這個新函式中!這是它的樣子:
? modelWrapper.js
import { computed } from 'vue'
export function useModelWrapper(props) {
return computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
}
好吧,這很簡單,但我們能做得更好嗎? 是的,我們可以!
不過為了理解我們可以用什麼方式,我們將繞一小圈回到 v-model
如何在元件上工作……
在 Vue 3 中,v-model
可以使用額外的引數將其應用到 modelValue
以外的屬性上。如果元件想要公開多個屬性作為 v-model
的目標,這是非常有用的。
v-model:argument
<message-editor
v-model="message"
v-model:draft="isDraft"
/>
第二個 v-model 可以這樣實現:
<input
type="checkbox"
:checked="draft",
@change="(event) => $emit('update:modelValue', event.target.checked)"
>
再重複一遍冗長的模板程式碼。是時候調整新的 composition 函式,這樣我們也可以在這個例子中重用它。
要實現這一點,我們需要向函式新增第二個引數,該函式指定我們實際想要封裝的屬性名稱。由於 modelValue
是 v-model
的預設屬性名稱,我們也可以將它作為包裝器的第二個引數的預設名稱:
? modelWrapper.js
import { computed } from 'vue'
export function useModelWrapper(props, name = 'modelValue') {
return computed({
get: () => props[name],
set: (value) => emit(`update:${name}`, value)
})
}
就是這樣。現在我們可以對任何 v-model
屬性使用這個包裝器。
所以最終的元件看起來是這樣的:
<template>
<label>
<input type="text" v-model="message" >
<label> <label>
<input type="checkbox" v-model="isDraft"> Draft
</label>
</template>
<script>
import { useModelWrapper } from '../utils/modelWrapper'
export default {
props: {
modelValue: String,
draft: Boolean
},
setup(props, { emit }) {
return {
message: useModelWrapper(props, 'modelValue'),
isDraft: useModelWrapper(props, 'draft')
}
}
}
</script>
下一步
這種可組合性不僅在我們希望將 modelValue
屬性對映到模板中的輸入時有用,還可以使用它將計算屬性引用傳遞給其他需要引用的可賦值組合。
透過像上面那樣,首先包裝 modelValue
屬性,其次組合函式可以不知道我們實際上沒有處理區域性狀態的事實。我們在可組合的小 useModelWrapper
中抽象了實現細節,因此其他可組合的可以將其視為本地狀態。
“快速輸入,否則丟失”
作為一個公認的愚蠢示例,我們有一個名為 useMessageReset
的可組合元件。當你停止輸入 5 秒後,它會將你的資訊重置為空字串。它是這樣的:
function useMessageReset(message) {
let timeoutId
const reset = () => {
if (timeoutId) {
clearTimeout(timeoutId)
}
timeoutId = setTimeout(() => (message.value = ''), 5000)
watch(message, () => message.value !== '' && reset())
}
}
此可組合使用 watch(),這是 Composition API 的另一個功能。
- 每當訊息更改時,第二個引數中的回撥函式就會執行。
- 如果訊息不是空的,它將(重新)啟動超時,5 秒後將訊息重置為空值。
注意,這個函式期望接收一個它可以監視的引用併為其賦值。
我們在使用 modelValue
屬性時會遇到問題,因為我們不能直接寫入它。但使用 useModelWrapper
,我們可以提供一個可寫的計算屬性引用到這個組合:
import { useModelWrapper } from '../utils/modelWrapper'
import { useMessageReset } from '../utils/messageReset'
export default {
props: { modelValue: Boolean },
setup(props, { emit }) {
const message = useModelWrapper(props)
useMessageReset(message)
return { message }
}
}
注意,這個可組合元件是如何不知道 message
實際上是為父元件的 v-model
發出一個事件的。就像我們透過普通的 ref() 一樣,它只能分配給 .value
。
還要注意,我們的其餘功能是如何不受此影響的。我們仍然可以在模板中使用 message
,或者以其他方式使用它。
最後的感想
對於每個想要使用它的 Vue 開發人員來說,composition API 是一個偉大的、靈活的工具。上面顯示的只是冰山一角。
本作品採用《CC 協議》,轉載必須註明作者和本文連結