1.回顾mixin基础
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
1.数据对象在内部会进行递归合并时,当发生冲突时以组件数据优先。
var mixin = {
data: function () {
return {
message: 'mixin',
foo: 'mixin'
}
}
}
new Vue({
mixins: [mixin],
data: function () {
return {
message: 'goodbye',
bar: 'def'
}
},
created: function () {
console.log(this.$data)
// 输出的是组件数据而不是混入数据
// => { message: "goodbye", foo: "abc", bar: "def" }
}
})
2.混入对象的钩子将在组件自身钩子之前调用。
var mixin = {
created: function () {
console.log('混入对象的钩子被调用')
}
}
new Vue({
mixins: [mixin],
created: function () {
console.log('组件钩子被调用')
}
})
// => "混入对象的钩子被调用"
// => "组件钩子被调用"
3.值为对象的选项,例如 **methods**、**components** 和 **directives**,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
var mixin = {
methods: {
foo: function () {
console.log('foo')
},
conflicting: function () {
console.log('from mixin')
}
}
}
var vm = new Vue({
mixins: [mixin],
methods: {
bar: function () {
console.log('bar')
},
conflicting: function () {
console.log('from self')
}
}
})
vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"
更多详细请查看Vue Mixin
2.element-ui中的mixin
(1)emitter.js
源码:
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
var name = child.$options.componentName;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
// dispatch
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
// broadcast
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
};
解释:
emitter.js中定义的broadcast和dispatch两者的用法和vue1.0中的broadcast和dispatch相似。
先分析**broadcast**方法:
该方法用于往下传播事件,根据给定的组件名称和事件名称,向当前的实例的子组件$children往下递归,寻找对应名称的子组件触发事件。在找到对应的组件时,会停止往下递归。
**broadcast**方法在element-ui中被用的次数也不少,例如在el-autocomplete的部分源码(如下所示):
// packages\autocomplete\src\autocomplete.vue
watch: {
suggestionVisible(val) {
let $input = this.getInput();
if ($input) {
this.broadcast('ElAutocompleteSuggestions', 'visible', [val, $input.offsetWidth]);
}
}
},
监听suggestionVisible变量,当发生变化时,通过getInput获取输入框里面的内容,如果内容不为空,则触发子组件ElAutocompleteSuggestions的visible事件。
子组件el-autocomplete-suggestions的部分源码如下所示,在created周期函数中已经监听visible事件,因此当调用该实例的$emit方法触发visible事件时,$on中注册在visible事件下的的回调函数就会被调用。
// packages\autocomplete\src\autocomplete-suggestions.vue
created() {
this.$on('visible', (val, inputWidth) => {
this.dropdownWidth = inputWidth + 'px';
this.showPopper = val;
});
}
现在分析**dispatch**方法:
而dispatch方法则是往上传播事件,也是根据给定的组件名称和事件名称,向当前实例的父组件$parent往上递归,寻找对应名称的父组件触发事件。在找到对应的组件时,会停止往上递归。
举一个element-ui中用到dispatch的最直观的需求--表单校验,如图所示:
<el-form-item label="活动名称" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
在大部分表单组件,例如el-input的源码中,会有以下代码:
watch: {
value(val) {
this.$nextTick(this.resizeTextarea);
if (this.validateEvent) {
this.dispatch('ElFormItem', 'el.form.change', [val]);
}
},
...
}
当监听到value值变化时,如果属性validateEvent为true,则向当前实例的父组件el-form-item传播'el.form.change'事件以及value值。
而在el-form-item的源码中,有以下代码:
mounted() {
if (this.prop) {
......
this.addValidateEvents();
}
},
methods:{
...
addValidateEvents() {
const rules = this.getRules();
if (rules.length || this.required !== undefined) {
this.$on('el.form.blur', this.onFieldBlur);
this.$on('el.form.change', this.onFieldChange);
}
},
....
}
在mounted函数中根据prop是否为真而调用addValidateEvents方法,addValidateEvents中监听el.form.change事件,从而达到父组件因子组件的值的变化而调用rules校验的需求。
(2)focus.js
源码:
export default function(ref) {
return {
methods: {
focus() {
this.$refs[ref].focus();
}
}
};
};
解析:
用于使某个被ref标记的组件进入focus状态,是个简单又通用的方法。
(3)migrating.js
源码:
import { kebabCase } from 'element-ui/src/utils/util';
/**
* Show migrating guide in browser console.
*
* Usage:
* import Migrating from 'element-ui/src/mixins/migrating';
*
* mixins: [Migrating]
*
* add getMigratingConfig method for your component.
* getMigratingConfig() {
* return {
* props: {
* 'allow-no-selection': 'allow-no-selection is removed.',
* 'selection-mode': 'selection-mode is removed.'
* },
* events: {
* selectionchange: 'selectionchange is renamed to selection-change.'
* }
* };
* },
*/
export default {
mounted() {
if (process.env.NODE_ENV === 'production') return;
if (!this.$vnode) return;
const { props = {}, events = {} } = this.getMigratingConfig();
const { data, componentOptions } = this.$vnode;
const definedProps = data.attrs || {};
const definedEvents = componentOptions.listeners || {};
for (let propName in definedProps) {
propName = kebabCase(propName); // compatible with camel case
if (props[propName]) {
console.warn(`[Element Migrating][${this.$options.name}][Attribute]: ${props[propName]}`);
}
}
for (let eventName in definedEvents) {
eventName = kebabCase(eventName); // compatible with camel case
if (events[eventName]) {
console.warn(`[Element Migrating][${this.$options.name}][Event]: ${events[eventName]}`);
}
}
},
methods: {
getMigratingConfig() {
return {
props: {},
events: {}
};
}
}
};
解释:
此方法用于标记一些已经过期的属性props和事件event,当开发人员在开发模式中用到这些过期的属性或方法时,会通过console.warn在控制台输出提示。
例如在el-input的源码中,有以下代码:
methods:{
....
getMigratingConfig() {
return {
props: {
'icon': 'icon is removed, use suffix-icon / prefix-icon instead.',
'on-icon-click': 'on-icon-click is removed.'
},
events: {
'click': 'click is removed.'
}
};
},
....
}
即,如果在调用el-input时使用了icon,如:<el-input v-model="value" icon="anything">,则控制台会输出警告如下图所示:
(4)vue-popper.js
这个可以说是element-ui的精华。篇幅过长,所以拆成以下两篇文章去分析。