「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!」javascript
Vue
框架有一大特點,就是組件化。html
即咱們能夠把一個複雜的頁面,拆分紅一個個獨立的組件,這樣子更加便於維護和調試;再者,組件還有一個特定就是可複用性,咱們能夠將多個頁面的共有部分抽取成一個組件,好比導航欄、底部信息、輪播圖等等。前端
組件化的實現,有助於咱們提供開發效率、方便重複使用,簡化調試步驟,提高項目可維護性。vue
而組件化的實現,就避免不了組件之間的通訊,即數據傳輸和方法調用。並且現實開發中,不只僅只有父子組件,還會有兄弟組件、爺孫組件等等。java
咱們先簡單過一遍常見的組件通信方法。react
以前我寫過一篇關於
Vue2
的組件通訊方法的文章,相對比較詳細git
對於父組件向子組件傳遞數據的時候,咱們經常使用的就是屬性綁定。vuex
// 父組件
<comp :msg="Hello World"></comp>
// 子組件
<script> export default { props: { msg: { // 使用props接收msg屬性 type: String, // 進行類型判斷 default: '' // 設置默認值 } } } </script>
複製代碼
當子組件先調用父組件的方法的時候,咱們經常使用就是將父組件的方法綁定給子組件,而後子組件經過$emit
調用。
並且,咱們也能夠經過此方法,實現子組件向父組件進行傳值。
// 父組件
<comp @saySomething="saySomething"></comp>
<script> export default { methods: { saySomething(msg) { // 接收到子組件的參數 console.log(msg); } } } </script>
// 子組件
<script> export default { methods: { saySomething() { // 調用綁定的saySomething事件,而且將HelloWorld做爲參數傳過去 this.$emit('saySomething', 'HelloWorld') } } } </script>
複製代碼
當父組件想要調用子組件的方法的時候,咱們能夠先獲取子組件的實例,而後直接經過實例調用方法。
// 父組件
<comp ref="comp"></comp>
<script> export default { methods: { saySomething() { // 經過this.$refs.comp獲取到comp組件實例,而後直接調用其方法並傳入參數。 this.$refs.comp.saySomething("HelloWorld"); } } } </script>
// 子組件
<script> export default { methods: { saySomething(msg) { console.log(msg); } } } </script>
複製代碼
對於兄弟組件通訊,或者多級組件之間的通訊,常常都是使用事件總線去實現。
而事件總線不是Vue
原生自帶的,這些須要咱們本身去封裝或者找插件去實現。而它的實現原理其實很簡單,就是模仿原生的$emit
、$on
、$once
、$off
的實現。
在
vue2
中一般也會用new Vue()
去代替Bus
,但在vue3
就取消了$on
全局接口,就只能同本身實現或者使用插件
class Bus {
constructor(){
// 存放全部事件
this.callbacks = {}
}
// 事件綁定
$on(name, fn){
this.callbacks[name] = this.callbacks[name] || []
this.callbacks[name].push(fn)
}
// 事件派發
$emit(name, args){
if(this.callbacks[name]){
this.callbacks[name].forEach(cb => cb(args))
}
}
}
// main.js
Vue.prototype.$bus = new Bus()
// comp1
this.$bus.$on('saySomthing', (msg) => { console.log(msg) });
// comp2
this.$bus.$emit('saySomthing', 'HelloWorld');
複製代碼
對於複雜結構的組件通信,咱們能夠選擇VueX
去實現通信,這裏就很少講了。
$attrs
/$listeners
)$attrs
包含了父做用域中不做爲 prop 被識別 (且獲取) 的 attribute 綁定 (class
和 style
除外)。
$listeners
包含了父做用域中的 (不含 .native
修飾器的) v-on
事件監聽器。
這兩種經常使用於隔代通信的狀況上。
// 父組件
<comp1 :msg1="helloWorld" :msg2="HiWorld" @saySomething="saySomething"></comp1>
// 子組件 comp1
<comp2 v-bind="$attrs" v-on="$listeners"></comp2>
<!-- 此時的$attrs只存在msg1,由於msg2已經被props識別了 -->
<!-- 上面的代碼,等同於下列代碼 -->
<!-- <comp2 :msg1="$attrs.msg1" @saySomething="$listeners.saySomething"></comp2> -->
<script> export default { props: ['msg2'] } </script>
// 孫組件 comp2
<div @click="$emit('saySomething')">{{msg1}}</div>
<script> export default { props: ['msg1'] } </script>
複製代碼
$parent
/$root
/$children
咱們能夠經過$parent
、$root
、$children
分別獲取到父級組件實例、根組件實例、子組件實例。
$children
返回是一個數組,而且不能保證數組中子元素的順序。
咱們可使用這些接口,配合$on
和$emit
實現一些組件通信。
/* 兄弟組件使用共同祖輩搭橋 */
// comp1
this.$parent.$on('foo', handle)
// comp2
this.$parent.$emit('foo')
複製代碼
// slot通訊
<comp1>
<comp2></comp2>
</comp1>
// comp1
<div>
<slot></slot>
</div>
<script> export default { methods: { saySomething() { // 遍歷$children進行派發事件 this.$children.forEach(comp => comp.$emit('saySomething', 'HelloWorld')) } } } </script>
// comp2
<script> export default { mounted() { // 在mounted的事件進行事件綁定 this.$on('saySomething', (msg) => { console.log(msg) }) } } </script>
複製代碼
provide
和inject
可以實現祖先組件與後代組件之間的傳值,也就是說不管是多少代,只要是嵌套關係,均可以使用該屬性進行傳值。
// 祖先組件
provide() {
return {
msg: 'Hello World' // 提供一個msg屬性
}
}
// 後代組件
inject: ['msg']; // 注入屬性
mounted() {
console.log(this.msg);
}
複製代碼
下列代碼會有刪減,能夠到 github 查看源碼
咱們經過模仿一下ElementUI
的Form
表單實現,來實踐一下組件通訊。
咱們大體一個Form
組件結構以下:
<o-form>
<o-form-item>
<o-input></o-input>
</o-form-item>
</o-form>
複製代碼
所以咱們先實現一下三個組件的頁面結構。
<!-- OForm.vue -->
<template>
<div>
<slot></slot>
</div>
</template>
<!-- OFormItem.vue -->
<template>
<div class="input-box">
<!-- 標籤 -->
<p v-if="label" class="label">{{ label }}:</p>
<slot></slot>
<!-- 錯誤提示 -->
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>
<!-- OInput.vue -->
<template>
<div>
<input>
</div>
</template>
複製代碼
首先咱們從最簡單的開始,實現input
的value
雙向綁定,這時候須要用到v-model
去實現。
<!-- app.vue -->
<template>
<o-form>
<o-form-item>
<o-input v-model="model.email" @input="input"></o-input>
</o-form-item>
</o-form>
</template>
<script> export default { data() { return { model: { email: '' } } }, methods: { input(value) { console.log(`value = ${value},this.model.email = ${this.model.email}`); } } } </script>
<!-- OInput.vue -->
<template>
<div>
<input :value="value" @input="input">
</div>
</template>
<script> export default { props: { value: { type: String } }, methods: { input(e) { // 派發input事件 this.$emit('input', e.target.value); } } } </script>
複製代碼
經過上面咱們但是實現最簡單的雙向綁定,也實現了OInput
組件的input
事件。
固然咱們能夠順便實現一下input
的其餘屬性,好比placeholder
、type
等等,固然這些屬性可使用$attrs
來實現,這樣子就不須要一個個props
出來。
<!-- app.vue -->
<template>
<o-form>
<o-form-item>
<o-input v-model="model.email" @input="input" type="email" placeholder="請輸入郵箱"></o-input>
</o-form-item>
</o-form>
</template>
<!-- OInput.vue -->
<template>
<div>
<!-- 使用$attrs綁定input其它屬性 -->
<input :value="value" @input="input" v-bind="$attrs">
</div>
</template>
<script> export default { inheritAttrs: false, // 不繼承默認屬性 ... } </script>
複製代碼
接下來也實現一下o-form-item
的屬性綁定,這個組件出現簡單顯示label
和錯誤信息以外,其實還有一個功能,就是數據校驗,這個在後面再細講。
這個組件默認傳入兩個屬性,一個是label
,一個是prop
,prop
主要適用於後面數據校驗判斷該form-item
是對應哪一個數據。
<!-- app.vue -->
<template>
<o-form>
<o-form-item label="郵箱" prop="email">
<o-input v-model="model.email" @input="input" type="email" placeholder="請輸入郵箱"></o-input>
</o-form-item>
<o-form-item label="密碼" prop="password">
<o-input v-model="model.password" placeholder="請輸入密碼" type="password"/>
</o-form-item>
</o-form>
</template>
<!-- OFormItem.vue -->
<script> export default { props: { label: { type: String, default: '' }, prop: { // 用於判斷該item是哪一個屬性 type: String, default: '' } }, data() { return { error: '' // 錯誤信息 } } } </script>
複製代碼
同時將檢驗規則rules
和model
傳入給OForm
。
<!-- app.vue -->
<template>
<o-form :model="model" :rules="rules">
<o-form-item label="郵箱" prop="email">
<o-input v-model="model.email" @input="input" type="email" placeholder="請輸入郵箱"></o-input>
</o-form-item>
<o-form-item label="密碼" prop="password">
<o-input v-model="model.password" placeholder="請輸入密碼" type="password"/>
</o-form-item>
</o-form>
</template>
<script> export default { data() { return { model: { email: '', password: '' }, // 校驗規則 rules: { email: [ {required: true, message: "請輸⼊郵箱"}, // 必填 {type: 'email', message: "請輸⼊正確的郵箱"} // 郵箱格式 ], password: [ {required: true, message: "請輸⼊密碼"}, // 必填 {min: 6, message: "密碼長度很多於6位"} // 很多於6位 ] } } } } </script>
<!-- OForm.vue -->
<script> export default { props: { model: { type: Object, required: true // 必填項 }, rules: { type: Object } } } </script>
複製代碼
如今基本的組件傳參已經實現了,接下來咱們就要來實現一下校驗功能。
首先,咱們在輸入的過程當中,就要開始調用數據檢驗了,所以在OInput
組件中的input
方法,須要調用到OFormItem
的檢驗方法。但由於是使用slot
嵌套,因此咱們可使用$parent
去派發事件。
// OInput.vue
input(e) {
// 派發input事件
this.$emit('input', e.target.value);
// 派發validate事件
this.$parent.$emit('validate');
}
// OFormItem.vue
mounted() {
// 在mounted鉤子實現事件綁定
this.$on('validate', () => {this.validate()});
},
methods: {
// 校驗方法
validate() {}
}
複製代碼
緊接着就來實現validate
方法。
首先咱們須要從OForm
組件拿到對應的值和規則,由於咱們已經有prop
值,所以咱們只須要拿到OForm
的model
和rules
屬性便可,而後經過prop
獲取對應的值和規則。
而這時,咱們就可使用到provide
和inject
來實現。
// OForm.vue
provide() {
return {
form: this // 返回整個實例
}
}
// OFormItem.vue
inject: ['form'], // 注入
methods: {
// 校驗方法
validate() {
// 獲取對應的值和規則
const value = this.form.model[this.prop];
const rules = this.form.rules[this.prop];
}
}
複製代碼
這個校驗使用了async-validator,這裏就簡單帶過。
<!-- OFormItem.vue -->
<script> import Schema from "async-validator"; export default { ... methods: { validate() { // 獲取對應的值和規則 const value = this.form.model[this.prop]; const rules = this.form.rules[this.prop]; // 建立規則實例 const schema = new Schema({[this.prop]: rules}); // 調用實例方法validate進行校驗,該方法返回Promise return schema.validate({[this.prop]: value}, errors => { if (errors) { // 顯示錯誤信息 this.error = errors[0].message; } else { this.error = ''; } }) } } } </script>
複製代碼
最後一個功能,就是提交表單的時候,須要所有表單校驗一遍。所以點擊提交按鈕的時候,須要調用到OForm
裏的校驗方法。
<!-- app.vue -->
<template>
<o-form :model="model" :rules="rules">
<o-form-item label="郵箱" prop="email">
<o-input v-model="model.email" @input="input" type="email" placeholder="請輸入郵箱"></o-input>
</o-form-item>
<o-form-item label="密碼" prop="password">
<o-input v-model="model.password" placeholder="請輸入密碼" type="password"/>
</o-form-item>
<o-form-item>
<button @click="register">註冊</button>
</o-form-item>
</o-form>
</template>
<script> export default { ... methods: { register() { // 調用form組件的validate方法 this.$refs.form.validate(valid => valid ? alert('註冊成功') : ''); } }, } </script>
複製代碼
而OForm
組件中的validate
方法,須要遍歷調用每一個OFormItem
的validate
方法,而且將結果方法。
// OForm.vue
validate(cb) {
const tasks = this.$children
.filter(item => item.prop) // 遍歷$children,篩選掉沒有prop值的實例
.map(item => item.validate()); // 調用子組件的validate方法
// 由於OFormItem的validate方法返回的是Promise,所以經過Promise.all判斷是否全都經過
Promise.all(tasks)
.then(() => cb(true))
.catch(() => cb(false))
}
複製代碼
這時咱們的Form
組件就基本實現了。
在Vue3
中,組件通信的方法發生了很多變化。
$on
、$once
、$off
Vue3
再也不支持$on
、$once
、$off
這三個方法,而當咱們必須使用此類方法的話,能夠經過本身封裝EventBus
事件總線或者使用第三方庫實現。
官方也推薦了mitt和tiny-emitter這兩個庫,使用方法也比較簡單,能夠本身去研究一下。
$children
Vue3
同時也移除了$children
方法,官方推薦是使用$refs
去實現獲取子組件的實例。
而Vue3
在composition api
中實現$refs
也有所不一樣,由於在setup
中的this
不是指向組件實例,所以咱們不能直接經過this.$refs
來獲取組件實例。
所以,下面簡單寫一下新的實現方法:
<template>
<comp ref="comp"></comp>
</template>
<script> import {ref, onMounted} from "vue"; export default { setup() { const comp = ref(); // 該變量名必須與上面綁定的名稱一致,並初始化的值爲空或爲null onMounted(() => { // 在mounted鉤子的時候,Vue會將該實例賦值給comp // 但若是你在mounted生命週期前訪問該值仍是爲空的 console.log(comp.value); }) return { comp // 必定得將該屬性暴露出去,不然Vue不會將子組件實例賦值給它 } } } </script>
複製代碼
若是在Vue3
依舊使用option api
的話,依舊可使用this.$emits
以及provide
、inject
選項;但若是使用compsition api
的話,emits
方法會經過setup
參數參入,而provide
和inject
能夠經過引入鉤子實現。
<script> import {provide, inject} from "vue"; export default { setup(props, {attrs, slots, emit}) { // 派發事件 emit('saySomething', 'Hello World'); // 提供屬性 provide('msg', 'Hello World'); // 注入屬性 const msg = inject('msg'); } } </script>
複製代碼
下列代碼會有刪減,能夠到 github 查看源碼
結構樣式跟上面Vue2
實現同樣,重複的東西我就很少講,重點是在於後面數據校驗的實現上,那部分後面會詳細講一講。
首先看看app.vue
的結構,樣式結構沒有太大變化,而這邊使用了composition api
寫法。
<template>
<div class="form">
<h1 class="title">用戶註冊</h1>
<o-form :model="model" :rules="rules" ref="formRef">
<o-form-item label="郵箱" prop="email">
<o-input v-model="model.email" @input-event="input" placeholder="請輸入郵箱" type="email" />
</o-form-item>
<o-form-item label="密碼" prop="password">
<o-input v-model="model.password" placeholder="請輸入密碼" type="password" />
</o-form-item>
<o-form-item>
<button @click="register">註冊</button>
</o-form-item>
</o-form>
</div>
</template>
<script> import OInput from "./components/OInput.vue"; import OFormItem from "./components/OFormItem.vue"; import OForm from "./components/OForm.vue"; import {ref, reactive} from "vue"; export default { name: 'App', components: { OInput,OFormItem,OForm }, setup() { // 表單數據 const model = reactive({ email: '', password: '' }) // 表單規則 const rules = reactive({ email: [ {required: true, message: "請輸⼊郵箱"}, {type: 'email', message: "請輸⼊正確的郵箱"} ], password: [ {required: true, message: "請輸⼊密碼"}, {min: 6, message: "密碼長度很多於6位"} ] }) // input方法 const input = (value) => { console.log(`value = ${value},model.email = ${model.email}`); } // 獲取OForm的實例 const formRef = ref(); // 提交事件 const register = () => { // 由於點擊事件會發生在mounted生命週期後,所以formRef已經被賦值實例 formRef.value.validate(valid => valid ? alert('註冊成功') : ''); } return { model, rules, input, register, formRef } } } </script>
複製代碼
接着來看看其餘組件的基本實現。
<!-- OInput.vue -->
<template>
<input v-model="modelValue" v-bind="$attrs" @input="input">
</template>
<script> export default { name: "OInput", props: { // Vue3中,v-model綁定的值默認爲modelValue,而再也不是value modelValue: { type: String } }, setup(props, {emit}) { const input = (e) => { const value = e.target.value // 派發事件 emit('inputEvent', value); } return { input } } } </script>
複製代碼
<!-- OFormItem.vue -->
<template>
<div class="input-box">
<p v-if="label" class="label">{{ label }}:</p>
<slot></slot>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>
<script> import {ref} from "vue"; export default { name: "OFormItem", props: { prop: { type: String, default: '' }, label: { type: String, default: '' } }, setup() { // error響應式變量初始化 const error = ref(''); return { error } } } </script>
複製代碼
<!-- OForm.vue -->
<template>
<div>
<slot></slot>
</div>
</template>
<script> export default { name: "OForm", props: { model: { type: Object, required: true }, rules: { type: Object, default: {} } } } </script>
複製代碼
如今基本的組件結構就實現了。
緊接着第一件事就是實如今OInput
組件中,在input
方法可以調用OFormItem
的校驗方法。
而在vue2
中,咱們是經過this.$parent.$emit
去派發實現,可是在vue3
的composition api
中顯然是不太好這麼去實現的,由於在setup
中獲取不到$parent
方法,何況在OFormitem
中也使用不了$on
去綁定事件。
所以,咱們可使用provide
和inject
的方法,將檢驗方法傳遞給OInput
組件,而後它直接調用就能夠了。
// OFormItem.vue
import {provide} from "vue";
export default {
setup() {
...
// 校驗方法
const validate = () => {}
// 提供validate方法
provide('formItemValidate', validate);
...
}
}
// OInput.vue
import { inject } from 'vue'
export default {
setup(props, {emit}) {
// 注入formItemValidate
const validate = inject('formItemValidate');
const input = (e) => {
const value = e.target.value
emit('inputEvent', value);
// 調用數據檢驗
validate();
}
return {
input
}
}
}
複製代碼
緊接着,咱們就要實現OFormItem
的校驗方法,首先要獲取到OForm
的model
和rules
屬性,一樣使用provide
和inject
的方法去實現。
// OForm.vue
import {provide} from "vue";
export default {
setup({model, rules}) {
// 向下提供model和rules,此時model和rules自己就是響應式數據,所以子組件注入的時候也是響應式數據
provide('model', model);
provide('rules', rules);
...
}
}
// OFormItem.vue
import {inject} from "vue";
import Schema from "async-validator"
export default {
...
setup({prop}) {
...
// 注入model和rules
const model = inject('model');
const rules = inject('rules');
// 校驗方法
const validate = () => {
// 獲取對應的值和校驗規則
const value = model[prop];
const rule = rules[prop];
// 進行校驗
const schema = new Schema({[prop]: rule});
return schema.validate({[prop]: value}, errors => {
if (errors) {
error.value = errors[0].message;
} else {
error.value = '';
}
})
}
...
}
}
複製代碼
最後呢,就是提交表單的時候,須要校驗全部的表單數據是否經過。
在app.vue
中,經過$refs
的方法調用OForm
的校驗方法。
// 獲取OForm的實例
const formRef = ref();
// 提交事件
const register = () => {
// 由於點擊事件會發生在mounted生命週期後,所以formRef已經被賦值實例
formRef.value.validate(valid => valid ? alert('註冊成功') : '');
}
複製代碼
而最難實現的就是OForm
的validate
方法。
在vue2
中,咱們是直接使用this.$children
進行遍歷執行就能夠了,可是在vue3
中,咱們沒有了$children
方法,並且官方推薦的$refs
方法也沒辦法使用,由於咱們使用的是slot
插槽,沒法綁定每一個OFormItem
上。
這時候,咱們須要使用事件總線來實現這個方法。
這裏我採用的是本身簡單寫一個EventBus
。
// utils/eventBus.js
const eventBus = {
callBacks: {},
// 收集事件
on(name, cb) {
if(!this.callBacks[name]){
this.callBacks[name] = [];
}
this.callBacks[name].push(cb);
},
// 派發事件
emit(name, args) {
if(this.callBacks[name]) {
this.callBacks[name].forEach(cb => cb(args));
}
}
}
export default eventBus
複製代碼
緊接着,咱們採用的方案是,在OForm
組件中,收集每一個OFormItem
的實例上下文,而後咱們就能夠直接調用對應實例上下文的validate
方法既可。
這個方案有點相似於Vue
源碼中的依賴收集。
咱們須要在OFormItem
組件初始化的時候,即mounted
生命週期的時候,派發一下收集事件,並將該組件的組件實例上下文做爲參數傳遞過去;即通知OForm
的收集,將傳入的上下文收集起來。
而在OForm
中,咱們須要在setup
中實現事件綁定,而不該該在OnMounted
鉤子實現,由於子組件的OnMounted
鉤子會比父組件的OnMounted
先調用,而咱們須要在事件派發前先綁定事件。
// OFormItem.vue
import {onMounted, getCurrentInstance} from "vue";
import eventBus from "../utils/eventBus"
export default {
setup() {
...
onMounted(() => {
// 在mount週期派發collectContext,讓OForm收集該組件上下文
const instance = getCurrentInstance();
eventBus.emit('collectContext', instance.ctx);
})
return {
...
validate // 方法必須返回出去,反正OForm獲取到的OFormItem實例沒法調用該方法
}
}
}
// OForm.vue
import eventBus from "../utils/eventBus"
export default {
...
setup({model, rules}) {
...
// 在mount聲明以前收集collectContext事件
const formItemContext = [];
eventBus.on('collectContext', (instance) => formItemContext.push(instance));
const validate = (cb) => {
// 遍歷收集到的子組件上下文,調用其校驗方法
const tasks = formItemContext
.filter(item => item.prop)
.map(item => item.validate())
Promise.all(tasks)
.then(() => cb(true))
.catch(() => cb(false))
}
return {
validate
}
}
}
複製代碼
這時候,咱們的Vue3
版本表單組件就實現了。