com组件的ref有时需要有时不需要?_Vue3组件通信总结
前言
我們知道vue3的Composition Api是它幾個最大亮點之一,所以下文都是在setup中演示代碼的實現。后面會以開發幾個簡單form組件為例子來演示。
基本操作
這里先簡單開發一個VInput的輸入框組件。組件就像一個函數,主要就是處理輸入和輸出。Vue3在setup函數上提供了兩個參數,一個props,一個是context下面的emit方法,分別來處理輸入和輸出。
props
現在VInput就是子組件,我需要它能夠接受父級傳遞一個值,讓它可以幫我做后續的邏輯處理在返回給父級。所以,這里需要最基本的一些父子通信方式v-bind,props。
父級組件中
// 通過v-bind將數據想子組件傳遞:value="valueRef" />const valueRef = ref('')
復制代碼
VInput中
:value="value" type="text" />復制代碼
emit
當我們在組件中接受參數,進行一些邏輯處理后,我們就需要將處理好的值,向外部進行一個返回,外部同時需要實現一個事件函數去接受。此時我就可以使用emit方法
假設我們希望VInput組件返回給外部的是一個限制長度的字符串。此時外部就需要實現一個對應的事件函數去接收這個值,然后VInput內部通emit執行事件,將內部的處理好的值當做參數返回出去。
VInput
:value="value" type="text" @input="onInput" ref="inputRef" />復制代碼
父級組件
// 通過v-on向子組件傳遞一個函數,用戶接受返回值:value="valueRef" :maxLength="10" @onInput="onInput" />復制代碼
對于這種input的組件的使用,我猜大家肯定都不想在父級組件這么麻煩的去接收和改變一個值,所以vue是提供了v-model來更快捷的實現輸入和輸出。
v-model
通過Vue3的文檔可以發現,這個指令的用法發生了一定的變化。在之前,我們要想實現一個自定義的非表單組件的雙向綁定,需要通過xxxx.sync的這種語法來實現,如今這個指令已經被廢除了,而是統一使用v-model這個指令。
父級組件
新的v-model?還可以支持多個數據的雙向綁定。
v-model:value="valueRef" v-model:keyword="keywordRef" />復制代碼
自定義的非表單組件
click="clickHandle">clickexport default defineComponent({
name: 'VBtn',
props: {
value: String,
keyword: String
},
setup(props, { emit }) {
// 省略其他代碼
// 用戶點擊按鈕
const clickHandle = (e: any) => {
// 省略其他代碼
// 修改對應的props的數據
emit('update:value', value)
emit('update:keyword', value + '123')
}
return {
// ...
}
}
})
復制代碼
以上就是在Vue3中一些基本通信方式的API的介紹。在Vue3中一般都是采用Composition Api的形式開發,所以你會發現開發的時候不能在采用this.$xxx的方式去調用實例上的某個函數或者是屬性。那些this.$parent,this.$children,this.$on,this.$emit等等都不能在使用了。
那在Vue3中如何解決組件間那些通信的呢?咱們從簡單到復雜的場景,一個個來分析。
先來看一下,開發的三個form組件,組合起來的實際的用法是怎么樣的:
ref="validateFormRef1" :model="state" :rules="rules">label="用戶名" prop="keyword">placeholder="請輸入"requiredv-model:modelValue="state.keyword"/>label="密碼" prop="password">placeholder="請輸入"requiredtype="password"v-model:modelValue="state.password"
/>class="btn btn-primary" @click="submit(0)">提交
復制代碼
所有組件的功能,是模仿Element UI去實現的。
父傳子
父組件向子組件傳遞一個數據,可以用這兩種方式:
v-bind
refs獲取子組件內部某個函數,直接調用傳參(這里簡稱refs方式)
refs方式
關于v-bind咱們就不細說了,在基本操作章節已經講過其對應的使用方式了。這小節主要在中講Vue3如何通過ref獲取子組件實例并調用其身上的函數來對子組件進行傳值。
子組件
// 渲染從父級接受到的值Son: {{ valueRef }}復制代碼
父組件
sonRefclick="sendValue">send// 這里ref接受的字符串,要setup返回的ref類型的變量同名ref="sonRef" />復制代碼
這里可以看一下流程圖:??其實這種方式跟Vue2中使用this.$refs,this.$children的方式很相似,都是通過拿到子組件實例,直接調用子組件身上的函數。方法千篇一律,不過在Vue3中沒有了this這個黑盒。
這里我們可以在控制臺看一下這個sonRef.value是一個怎樣的東西。
可以發現,通過ref獲取到的子組件實例上面可以拿到setup返回的所有變量和方法,同時還可以拿到其他的一些內部屬性。我們可以看一下官方文檔Vue 組合式 API的描述。
在 Virtual DOM patch 算法中,如果一個 VNode 的 ref 對應一個渲染上下文中的 ref,則該 VNode 對應的元素或組件實例將被分配給該 ref。這是在 Virtual DOM 的 mount / patch 過程中執行的,因此模板 ref 僅在渲染初始化后才能訪問。
ref方式總結
優點:
父組件可以獲取快速向確定存在的子組件傳遞數據
傳遞的參數不受限制,傳遞方式比較靈活
缺點:
ref獲取的子組件必須確定存在的(不確定存在的情況:如插槽上子組件,v-if控制的子組件)
子組件還需要實現接受參數的方法
父傳更深的后代
一般往深度層級傳遞值,有這兩種方式:
provide / inject
vuex
provide / inject
一看到“深”這個字,大家肯定第一想到的就Vue2中的provide / inject選項。沒錯,這套邏輯在vue3中同樣適用,這兩個選項變成了兩個方法。
provide允許我們向當前組件的所有后代組件,傳遞一份數據,所有后代組件能夠通過inject這個方法來決定是否接受這份數據。
大致的示意圖如下:?
實際應用場景
主要應用的場景有兩中,一種深度傳遞一個參數或者一個函數的時候,另一種是給插槽上不確定性的組件傳參的時候。
重點說一下給插槽上的組件傳參。先實現一個最外層的ValidateForm組件,它主要負責接受一整個表單數據和整個表單數據的校驗規則。其內部提供了一個插槽,用于放置一些不確定性的組件。還有一個ValidateFormItem組件可以接受一個字段名,通過這字段名準確知道需要校驗哪個字段(tips:功能其實和element-ui類似)。
組件化開發,需要將參數和功能進行解耦,所以我們這樣來設計:
ValidateForm:model,rules,只管接受整份表單的數據和校驗規則
ValidateFormItem:prop,只管接受字段名,只需知道自己需要驗證哪一個字段
復制代碼
如果ValidateFormItem組件需要通過prop去效驗某個字段,那它就需要拿到那份表單的數據,通過formData[prop]去取到那個字段的值,那這份formData從哪里來呢?首先不可能每寫一個ValidateFormItem組件都傳遞一份。因為,實際開發中我們并不能確定在ValidateForm下要寫多少個ValidateFormItem組件,如果每寫一個都手動傳遞一份表單的數據,這些寫起來就會多了很多冗余的代碼而且也很麻煩。所以,就由ValidateForm這個組件獨立接受并分發下來。
ValidateForm
所以我們需要ValidateForm來向下分發數據。
復制代碼
ValidateFormItem
ValidateFormItem接受上面傳遞的數據。
復制代碼
provide / inject總結
在這篇文章Vue組件通信方式及其應用場景總結中,大佬對其的優缺點已經總結很好了。這里提一下它的缺點,就是不能解決兄弟組件的通信。
vuex
vuex一直以來是vue生態中一個解決不同層級組件數據共享的優質方案。不僅是在父傳子中可以適用,在子傳父,或者祖先傳后代,后代傳祖先,兄弟組件間都是一個非常好的方案。因為它是一個集中狀態管理模式。其本質實現也是響應式的。這里只簡單提一下Vue3中是如何使用的。
創建一個store
import { createStore } from 'vuex'export enum Mutarions {
SET_COUNT = 'SET_COUNT'
}
export default createStore({
state: {
count: 231
},
getters: {
count: state => state.count
},
mutations: {
[Mutarions.SET_COUNT]: (state, num: number) => (state.count = num)
}
})
復制代碼
父組件
fatherref="sonRef" />復制代碼
子組件
Son: {{ count }}復制代碼
子傳父
子級向父級傳遞數據,可以有這三種方式:
v-on
refs方式
事件中心
refs方式
通過ref的方式向父級傳遞一個數據是同樣適用的。具體思路:子組件內部實現一個函數,該函數可以返回一個值。父級組件通過ref取到子組件實例后調用該方法,得到需要的返回值。
這里來看一下實際的應用場景,我們希望ValidateForm組件去驗證下面所有的表單項,然后通過一個函數將組件內部的一個驗證狀態返回出去。
父組件
ref="validateFormRef" :model="formData" :rules="rules">label="用戶名" prop="keyword">label="密碼" prop="password">復制代碼
ValidateForm
復制代碼
這里來看一下大致的流程圖:
通過該種方法還可以拿到子組件內部的數據,這就跟閉包函數一樣的道理。
事件中心
這種通信方式為什么拿到這里來講呢?因為我覺接下的實際案例用上事件中心這種方式會非常的恰當。在上一個小節中,我們留下來一個坑,那就是ValidateForm組件要去驗證整個表單是否通過,就必須想辦法讓每個ValidateFormItem將內部的校驗結果返回給它。
首先會遇到兩個問題
ValidateForm下面的組件是通過插槽去掛載的,所以無法通過ref的方式去拿到每個子表單項的實例,所以就沒辦法拿到每個ValidateFormItem的驗證狀態了。
上面的章節中有一個圖片,展示了通過ref拿到的組件實例。可以發現,你可以找到$parent屬性,但是沒有$children屬性。這就很尷尬了,我們沒辦法像Vue2一樣在ValidateForm中通過$children拿到每個子組件的實例。
解決思路
既然沒有辦法拿到插槽上的組件實例,那咱們就繞開它,通過一個事件中心的方式來解決。思路是這樣的:
在ValidateForm實例初始化的時候,去創建一個事件中心Emitter實例,它可以注冊一個事件,當這個事件被執行時可以接受一個函數,并存在一個隊列中。
將這個Emitter通過provide傳遞給后代,保證這個事件中心在不同的ValidateForm組件中都是獨立的。換句話說,就是如果寫了多個ValidateForm,他們的事件中心不會相互干擾。
在ValidateFormItem中使用inject接收自己所在表單域的Emitter,在掛載的時候,執行Emitter上的事件,將自己的內部的validate函數,傳遞發送給ValidateForm,并由其將方法緩存在隊列中。
ValidateForm執行校驗的時候,就可以執行隊列中的所有校驗函數,并得出校驗結果。
具體代碼實現:
先來實現一個Emitter事件中心的類
import { EmitterHandles } from '@/type/utils'export class Emitter {
// 存放事件函數
private events: EmitterHandles = {}
// 用于注冊事件
on(eventName: string, eventHandle: Function) {
this.events[eventName] = eventHandle
}
// 刪除事件
off(eventName: string) {
if (this.events[eventName]) {
delete this.events[eventName]
}
}
// 觸發事件
emit(eventName: string, ...rest: any[]) {
if (this.events[eventName]) {
this.events[eventName](...rest)
}
}
}
復制代碼
當事件中心實現好了,這里來完善一下ValidateForm的代碼
復制代碼
ok,現在實現了validateForm的邏輯,我們再來寫一下validateFormItem的邏輯
class="form-group">v-if="label" class=" col-form-label">{{ label }}v-if="error.isError" class="invalid-feedback">{{ error.errorMessage }}
復制代碼
為了更詳細的理解上面的過程,這里來畫一個示意圖:
注冊事件,分發事件中心
執行事件,發送驗證函數
整個過程的總結就是,頂層組件創建和分發事件中心,并注冊事件監聽函數。后代組件執行該事件然后發送信息,頂層組件回收信息。
Tips
這里再提一點,在使用Emitter這個事件中心的時候,是在ValidateForm的setup中去創建并且去下發的,并不是使用一個全局的事件中心。就像大佬的這篇文章Vue組件通信方式及其應用場景總結中總結到的,事件總線的形式是有一個致命缺點的,如果一個頁面上有多個公共組件,我們只要向其中的一個傳遞數據,但是每個公共組件都綁定了數據接受的方法,那就會出現混亂的情況。但是,我們的事件總線不是一個全局的,而是單個作用域里面的一個事件中心。
因為事件中心是在當前組件內部創建,并使用provide向下發布的,這樣就只有當前組件的后代才能使用這個事件中心。所以,就算一個面上寫了多個ValidateForm,他們的校驗都是獨立的。
ref="validateFormRef1" :model="formData1" :rules="rules">label="用戶名" prop="keyword">label="密碼" prop="password">ref="validateFormRef2" :model="formData2" :rules="rules">label="用戶名" prop="keyword">label="密碼" prop="password">復制代碼
示意圖:
事件中心總結
優點:
可以解決Vue3不能使用this.$children的問題
可以靈活使用,不受組件層級的限制
這種通信方式不受框架的限制
缺點:
需要控制好事件中心的作用范圍
需要控制好事件名的規范
事件中心進階
因為在Vue3的Composition API中,vue的功能api更加的顆粒化。我們可以對事件中心進行一個自定義需求的改造。
可以通過引入reactive, ref幫助我們的事件中心內部維護一個響應式的數據,可以實現當事件中心進行一定通信行為時,去更新對應的視圖。還可以引入computed實現計算屬性的功能。
import { reactive, ref, computed } from 'vue'export class Emitter {
// 響應式的數據中心
private state = reactive({})
private events: EmitterHandles = ref({})
// 記錄當前事件中心 事件的數量
private eventLength = computed(() => Object.keys(events.value).length)
// 省略部分代碼
}
復制代碼
加入watch,watchEffect實現數據監聽做出一定邏輯行為的功能。我認為Composition API和React Hooks Api都是非常強大,因為它們允許我們將功能函數當成積木一樣去任意組裝成我們希望得到的應用程序。
深層后代向頂層通信,兄弟通信
我覺得其實其他的場景,其通信方式基本都差不多了,所謂千篇一律。后代向祖先傳值,或者兄弟組件傳值,都可以使用vuex或者是事件中心的方式。兄弟層級,或者相鄰層級的,就可以使用ref,$parent等方式。
總結
以上是生活随笔為你收集整理的com组件的ref有时需要有时不需要?_Vue3组件通信总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 语音通话框架_教资公告还没出,普通话测试
- 下一篇: 从0搭建一个Springboot+vue