对比一下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版本表单组件就实现了。

相关文章
相关标签/搜索