「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,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
版本表单组件就实现了。