對比一下Vue2和Vue3的組件通訊實現

「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!javascript

Vue框架有一大特點,就是組件化html

即咱們能夠把一個複雜的頁面,拆分紅一個個獨立的組件,這樣子更加便於維護和調試;再者,組件還有一個特定就是可複用性,咱們能夠將多個頁面的共有部分抽取成一個組件,好比導航欄、底部信息、輪播圖等等。前端

組件化的實現,有助於咱們提供開發效率、方便重複使用,簡化調試步驟,提高項目可維護性。vue

而組件化的實現,就避免不了組件之間的通訊,即數據傳輸和方法調用。並且現實開發中,不只僅只有父子組件,還會有兄弟組件、爺孫組件等等。java

咱們先簡單過一遍常見的組件通信方法。react

Vue2組件通訊方法

以前我寫過一篇關於Vue2的組件通訊方法的文章,相對比較詳細git

屬性綁定(props)

cn.vuejs.org/v2/guide/co…github

對於父組件向子組件傳遞數據的時候,咱們經常使用的就是屬性綁定。vuex

// 父組件
<comp :msg="Hello World"></comp>

// 子組件
<script> export default { props: { msg: { // 使用props接收msg屬性 type: String, // 進行類型判斷 default: '' // 設置默認值 } } } </script>
複製代碼

事件綁定(on、emit)

cn.vuejs.org/v2/api/#%E5…後端

當子組件先調用父組件的方法的時候,咱們經常使用就是將父組件的方法綁定給子組件,而後子組件經過$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>
複製代碼

訪問子組件實例(ref)

cn.vuejs.org/v2/guide/co…

當父組件想要調用子組件的方法的時候,咱們能夠先獲取子組件的實例,而後直接經過實例調用方法。

// 父組件
<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

vuex.vuejs.org/zh/

對於複雜結構的組件通信,咱們能夠選擇VueX去實現通信,這裏就很少講了。

非prop特性($attrs/$listeners

cn.vuejs.org/v2/api/#vm-…

$attrs 包含了父做用域中不做爲 prop 被識別 (且獲取) 的 attribute 綁定 (classstyle 除外)。

$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

cn.vuejs.org/v2/api/#vm-…

咱們能夠經過$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

cn.vuejs.org/v2/api/#pro…

provideinject可以實現祖先組件與後代組件之間的傳值,也就是說不管是多少代,只要是嵌套關係,均可以使用該屬性進行傳值。

// 祖先組件
provide() { 
  return {
      msg: 'Hello World' // 提供一個msg屬性
    }
}

// 後代組件
inject: ['msg'];  // 注入屬性
mounted() {
  console.log(this.msg);
}
複製代碼

Vue2實現Form表單

下列代碼會有刪減,能夠到 github 查看源碼

咱們經過模仿一下ElementUIForm表單實現,來實踐一下組件通訊。

咱們大體一個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>
複製代碼

首先咱們從最簡單的開始,實現inputvalue雙向綁定,這時候須要用到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的其餘屬性,好比placeholdertype等等,固然這些屬性可使用$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,一個是propprop主要適用於後面數據校驗判斷該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>
複製代碼

同時將檢驗規則rulesmodel傳入給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值,所以咱們只須要拿到OFormmodelrules屬性便可,而後經過prop獲取對應的值和規則。

而這時,咱們就可使用到provideinject來實現。

// 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方法,須要遍歷調用每一個OFormItemvalidate方法,而且將結果方法。

// 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組件通信的改動

Vue3中,組件通信的方法發生了很多變化。

移除了$on$once$off

v3.cn.vuejs.org/guide/migra…

Vue3再也不支持$on$once$off這三個方法,而當咱們必須使用此類方法的話,能夠經過本身封裝EventBus事件總線或者使用第三方庫實現。

官方也推薦了mitttiny-emitter這兩個庫,使用方法也比較簡單,能夠本身去研究一下。

移除了$children

v3.cn.vuejs.org/guide/migra…

Vue3同時也移除了$children方法,官方推薦是使用$refs去實現獲取子組件的實例。

Vue3composition 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>
複製代碼

emits、provide、inject選項

若是在Vue3依舊使用option api的話,依舊可使用this.$emits以及provideinject選項;但若是使用compsition api的話,emits方法會經過setup參數參入,而provideinject能夠經過引入鉤子實現。

<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>
複製代碼

Vue3實現Form表單

下列代碼會有刪減,能夠到 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去派發實現,可是在vue3composition api中顯然是不太好這麼去實現的,由於在setup中獲取不到$parent方法,何況在OFormitem中也使用不了$on去綁定事件。

所以,咱們可使用provideinject的方法,將檢驗方法傳遞給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的校驗方法,首先要獲取到OFormmodelrules屬性,一樣使用provideinject的方法去實現。

// 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('註冊成功') : '');
}
複製代碼

而最難實現的就是OFormvalidate方法。

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版本表單組件就實現了。

相關文章
相關標籤/搜索