在组件开发中,最难得环节应该是解耦组件的交互逻辑,尽量把复杂的逻辑分发到不同的子组件中,然后彼此建立联系,比如Vue中的计算属性(computed)和混合属性(mixins)等技术点的合理使用
Vue 组件的 props 校验 - validator
通常在编写通用组件时,
props的传递必不可少,在定义对应的props时,建议使用对象的形式,这样可以进行自定义校验规则(也包含常规的类型的设置、默认值)等;
定义一个通用的button组件
// 判断参数是否是其中之⼀
function oneOf(value, validList) {
for (let i = 0; i < validList.length; i++) {
if (value === validList[i]) {
return true;
}
}
return false;
}
export default {
props: {
size: {
validator(value) {
return oneOf(value, ["small", "large", "default"]);
},
default: "default",
},
checked: {
type: [String, Number, Boolean],
default: false
},
disabled: {
type: Boolean,
default: false,
},
},
};
组件间的通信可以参考ref $parent $children进行跨组件通信,但是在跨级和兄弟组件间通信时就得依赖其他插件或者工具了
v-model 语法糖
v-model常用于表单元素上进行数据的双向绑定,可以拆解为props:value和events:input,即在自定义的组件上使用v-model,必须满足组件提供一个名为value的prop和名为input的自定义事件
<InputNumber :value="value" @input="handleChange" />
<InputNumber v-model="value" />
实现原理
组件的设计理念是:组件内部不能更改props值,只能通过父组件修改,因此内部通常有一个内部data去维护数据,并通过watch进行监听data变化同时更新内部定义的data;组件内部修改自己维护的data数据时,会通过exit input事件到父组件,父组件通过@input进行监听后由父组件修改value;
model选项更改语法糖对应的key值
如果你不想⽤ value 和 input 这两个名字,从 Vue.js 2.2.0 版本 开始,提供了⼀个 model 的选项,可以指定它们的名字
<script>
export default {
name: 'InputNumber',
props: {
number: {
type: Number
}
},
model: {
prop:
'number',
event: 'change'
},
data() {
return {
currentValue: this.number
}
},
watch: {
value(val) {
this.currentValue = val;
}
},
methods: {
increase(val) {
this.currentValue += val;
this.$emit('number', this.currentValue);
}
}
}
// .sync 语法糖
// this.$emit('update:value', this.value + val)
// <InputNumber :value.sync="value" />
</script>
computed的set操作
常规的computed是采用函数的方式进行使用的,此时只是默认调用了他的get方法,当书写为对象格式时,就可以添加set方法了;
// 普通函数形式
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
// 对象形式
computed: {
fullName: {
get() {
return `${this.firstName} ${this.lastName}`;
},
set(val) {
const names = val.split(' ');
this.firstName = names[0];
this.lastName = names[names.length - 1];
}
}
}
无依赖组件间的通信 - provide/inject
provide和inject绑定并不是可响应的。这是刻意为之的。然⽽,如果你传⼊了⼀个可监听的对象,那么其对象的属性还是可响应的。其他情况下都不是响应式的,后续的更改不会导致子组件的inject更新;
用途主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供和依赖注入的关系
provide 全局父组件代替VueX等数据管理
可以在全局父组件里通过
provide将所有需要对外提供的全局属性方法进行透传,包括不限于计算属性、方法等,甚至将整个app.vue实例进行相应的透传;
// 全局组件
<template>
<div>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "App",
provide() {
return {
app: this,
};
},
};
</script>
// 子组件
<template>
<div>
{{ app.userName }}
</div>
</template>
<script>
export default {
name: "user",
inject: ["app"],
methods: {
changeUserInfo() {
// 这⾥修改完⽤户数据后,通知 app.vue 进行相应的更新;
$.ajax("/dataApi/update", ({errno,data}) => {
errno === 0 && (this.App.getUserInfo(data))
});
},
}
};
</script>
利用mixins进行独立逻辑的抽离
当进行协同开发、
全局状态统一维护时,不同的逻辑往往都会堆积到一起,这样是不利于维护的,可以利用mixins进行相应独立逻辑的抽离;
// userInfo.js
export default {
data() {
return {
userInfo: {},
};
},
methods: {
getUserInfo() {
$.ajax("/dataApi/getUserinfo", (data) => {
this.userInfo = data;
});
},
},
mounted() {
this.getUserInfo();
},
};
// app.vue
<template>
<div>
{{ userInfo.userName }}
</div>
</template>
<script>
import mixins_user from "../mixins/user.js";
export default {
mixins: [mixins_user],
data() {
return {};
},
};
</script>
旧版本实现provide和inject功能 - 计算属性
通常的多级组件的通信可以通过
$parent/$children进行获取数据与方法调用,但是在层级比较深的组件中不能单纯的通过$parent/$children进行获取对应组件的实例,可以通过配合组件中的name配置和计算属性进行获取;
computed: {
parentTem() {
let parent = this.$parent;
while (parent.$options.name && parent.$options.name !== "app") {
parent = parent.$parent;
}
return parent;
},
},
provide() {
return {
parent: this.parentTem
};
},
vue 1.x中的$dispatch和$broadcast
相关简介
因为基于组件树结构的事件流方式实在是让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱。这种事件方式确实不太好,我们也不希望在以后让开发者们太痛苦。并且
$dispatch和$broadcast也没有解决兄弟组件间的通信问题。 -- vue官方
dispatch 是一个事件,首先会在自己实例本身上触发,然后沿父链向上传播。当它触发父组件上的事件侦听器时传播即会停止,除非该侦听器返回 true。 任何其他参数都将传递给侦听器的回调函数 -- vue官方
broadcast是一个事件,它向下传播到当前实例的所有后代。由于后代扩展为多个子树,事件传播将会遵循许多不同的“路径”。 除非回调返回 true,否则在沿该路径触发侦听器回调时,每个路径的传播将会停止。 -- vue官方
$dispatch和$broadcast适用于有祖先关系的组件间通信,原理是通过遍历组件树,根据组件名(或者其它组件唯一标识)定位到对应的组件,并在找到的组件实例中调用on监听的事件,这样就完成了一次组件间的通信。$dispatch是父组件遍历他下面的所有后代组件,而$broadcast的顺序则相反,是子组件向上寻找对应的父组件。
实现$dispatch和$broadcast
功能分析
- dispatch:子组件调用dispatch方法,向
指定的最近的上级组件实例触发自定义事件,并传递数据,但前提是该目标父组件已经提前监听了这个自定义的dispatch事件 - broadcast:父组件调用broadcast方法,向
指定的最近的下级组件实例触发自定义的事件,并传递数据,但前提是该目标子组件已经提前监听了这个已定义的broadcast事件
设计思路
- 关键点:正确的找到目标组件实例,并触发指定的事件
- 设计原理:可以先确定好该功能的相关信息,如方法名、参数、回调、使用样例等
- 拓展:可以采用mixins进行独立逻辑的提取,以便在尽可能多的组件中进行使用与相关的复用
具体实现
在进行指定组件实例的查找过程中,有两种方案进行;方案一:通过children获取到所有父节点或子节点,然后利用组件的方法事件进行判断,方案二:通过多传入一个目标组件名称(或唯一标识)进行目标组件查找,然后再在找到的目标组件实例上触发指定的事件;
//mixinsEvent.js
function broadcast(componentName, eventName, params) {
this.$children.forEach((child) => {
const name = child.$options.name;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.name;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.name;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
},
},
};
//componentA.vue
<template>
<button @click="handleClick">触发事件</button>
</template>
<script>
import Emitter from "../mixins/mixinsEvent.js";
export default {
name: "componentA",
mixins: [Emitter],
methods: {
handleClick() {
this.broadcast(
"componentB",
"on-message",
"Hello Vue.js"
);
},
},
};
</script>
// componentB.vue
export default {
name: "componentB",
created() {
this.$on("on-message", this.showMessage);
},
methods: {
showMessage(text) {
window.alert(text);
},
},
};
vue官方实现
vue官方是接受一个参数,通过该参数进行向上或向下进行遍历寻找,找到目标组件后会执行相应的指定事件,根据返回的是否为true进行判断是否继续向下或向上遍历通知
// Vue
/**
* Recursively broadcast an event to all children instances.
* 递归地向所有子实例广播事件。
* @param {String|Object} event
* @param {...*} additional arguments
*/
// $dispatch 方法是定义在 Vue 的 prototype 上的
// 接受一个事件
Vue.prototype.$broadcast = function (event) {
// 获取传入事件的类型,判断是否为字符串
var isSource = typeof event === "string";
// 校正 event 的值,当接受 event 的类型为字符串时就直接使用,如果不是字符串就使用 event 上的 name 属性
event = isSource ? event : event.name;
// if no child has registered for this event,
// then there's no need to broadcast.
// 如果当前组件的子组件没有注册该事件,就直接返回,并不用 broadcast
if (!this._eventsCount[event]) return;
// 获取当前组件的子组件
var children = this.$children;
// 将函数接受的参数转换成数组
var args = toArray(arguments);
// 如果传入事件为字符串
if (isSource) {
// use object event to indicate non-source emit
// on children
// 根据传入的事件名称的参数组装成 object
args[0] = { name: event, source: this };
}
// 循环子组件
for (var i = 0, l = children.length; i < l; i++) {
var child = children[i];
// 在每个子组件中调用 $emit 触发事件
var shouldPropagate = child.$emit.apply(child, args);
// 判断调用 $emit 返回的值是否为 true
if (shouldPropagate) {
// 如果调用 $emit 返回的值为 true,就递归孙子组件继续广播
child.$broadcast.apply(child, args);
}
}
// 最后返回当前组件的实例
return this;
};
父子组件渲染顺序,实例创建顺序
订阅必须先于发布,也就是说先有on再有emit
- 子组件先于父组件前渲染,所以在子组的mounted派发事件时,在父组件中的mounte中是监听不到的。
- 父组件的create是先于子组件的,所以可以在父组件中的create可以监听到
拓展
组件封装技巧与原则
组件封装前戏 - 接口设计
- 接口设计主要考虑到三个部分:
props、slots和events,通过将需要封装的组件进行精细化拆分后,- 利用slots进行相关组件的关联,父子孙等组件进行
slots拆分插入管理 - 利用events或者其他事件方法进行组件间通信与校验「
在组件拆分时应该进行解耦,每个组件只负责独立的功能,如相关数据校验和初始化等,需要进行组件间通信时常常进行父级组件缓存子组件实例的方案」 - 利用props进行组件间
数据传递
- 利用slots进行相关组件的关联,父子孙等组件进行
- 其他可以用到的如
ref等,可以按照组件实际应用场景和实现逻辑进行使用,如在最外层组件进行方法调用时就可以用ref进行获取实例(使用者进行手动调用根组件的API进行检验等方法调用),然后进行后续操作逻辑;
设计原则
- 组件间进行解耦,相互拆分后只负责单独独立的功能逻辑;
- 关联组件间通过指定的方式进行插入关联;
- 主组件进行全局数据及相关交互把控,必要时进行控制组件间通信与交互;
- 利用组件实例的缓存达到全局组件的整体把控;
- 组件设计时先设计组件的API,定义好接口,完事后再补全组件细节逻辑,对于使用者而言,只需要知道props、events和slots即可;
示例代码
<template>
<div>
<el-form :model="formValidate" :rules="ruleValidate">
<el-form-item label="⽤户名" prop="name">
<el-input v-model="formValidate.name"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="eMail">
<el-input v-model="formValidate.eMail"></el-input>
</el-form-item>
</el-form>
</div>
</template>
<script>
import elForm from "@/components/form/form.vue";
import elFormItem from "@/components/form/form-item.vue";
import elInput from "@/components/input/input.vue";
export default {
components: {
elForm,
elFormItem,
elInput
},
data() {
return {
formValidate: {
name: "",
eMail: "",
},
ruleValidate: {
name: [{ required: true, message: "⽤户名不能为空", trigger: "bpplur" }],
eMail: [
{ required: true, message: "邮箱不能为空", trigger: "blur" },
{ type: "email", message: "邮箱格式不正确", trigger: "blur" },
],
},
};
},
};
</script>
组件通信 - 指定节点组件实例获取
通过找到指定组件的实例,进而调用组件上的数据或方法,常见的需求是向上或向下找到最近的单个或所有的指定组件,或找到指定组件的兄弟组件 - 5种不同的应用场景
实现原理都是通过递归遍历后找到相匹配的实例并返回
向上找到最近的指定组件 - findComByUpward
function findComByUpward(current,target){
let parent = current.$parent,
name = parent.$options.name;
while(parnet && (!name || [target].indexOf(name) <0)){
// 没有找到指定组件
parent = parent.$parent;
parent && (name = parent.$options.name)
}
// 返回目标组件实例
return parent
}
// 示例
findComByUpward(this,'app')
向上找到所有的指定组件 - findComsByUpward
function findComsByUpward(current, target) {
let parents = [];
const parent = current.$parent;
if (parent) {
if (parent.$options.name === target) parents.push(parent);
return parents.concat(findComponentsUpward(parent, target));
} else {
return [];
}
}
向下找到最近的指定组件 - findComByDownward
function findComByDownward(current, target) {
// $children返回的是当前组件的所有子组件数组
let childrens = current.$children,
result;
if (childrens.length) {
for (const child of childrens) {
if (child.$options.name === target) {
result = child;
break;
} else {
result = findComByDownward(child, target);
if (!result) break;
}
}
}
return result;
}
向下找到所有的指定组件 - findComsByDownward
function findComsByDownward(current, target) {
return current.$children.reduce(
(components, child) => {
if (child.$options.name === target) components.push(child);
const nextChildren = findComsByDownward(child, target);
return components.concat(nextChildren);
},
[]);
}
找到指定组件的所有兄弟组件 - findComsByBrother
function findComsByBrother(current, target, excludeCurrent = true) {
let res = current.$parent.$children.filter((item) => {
return item.$options.name === target;
});
let index = res.findIndex((item) => item._uid === current._uid);
if (excludeCurrent) res.splice(index, 1);
return res;
}