基础
Vue.js 组件的三个 API:prop、event、slot
prop
如果你不希望组件的根元素继承特性,你可以在组件的选项中设置
inheritAttrs: false
。- https://zhuanlan.zhihu.com/p/195455758
slot
- 默认插槽
- 具名插槽
event
子:this.$emit('on-click', event);
父:<i-button @on-click="handleClick"></i-button>
组件引用
- ref:给元素或组件注册引用信息;
- $parent / $children:访问父 / 子实例。
组件通信1
provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。
// A.vue
export default {
provide: {
name: 'Aresn'
}
}
// B.vue
export default {
inject: ['name'],
mounted () {
console.log(this.name); // Aresn
}
}
B 是 A 的子组件,A的所有子集都可以用inject获取
provide 和 inject 绑定并不是可响应的,A.vue 的 name 如果改变了,B.vue 的 this.name 是不会改变的,仍然是 Aresn。
组件通信2
$on
监听当前实例上的自定义事件。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数。
vm.$on('test', function (msg) {
console.log(msg)
})
vm.$emit('test', 'hi')
// => "hi"
Vue.js 1.x 的 $dispatch 与 $broadcast
在 Vue.js 1.x 中,提供了两个方法:$dispatch 和 $broadcast ,前者用于向上级派发事件,只要是它的父级(一级或多级以上),都可以在组件内通过 $on (或 events,2.x 已废弃)监听到,后者相反,是由上级向下级广播事件的。
来看一个简单的示例:
<!-- 注意:该示例为 Vue.js 1.x 版本 -->
<!-- 子组件 -->
<template>
<button @click="handleDispatch">派发事件</button>
</template>
<script>
export default {
methods: {
handleDispatch () {
this.$dispatch('test', 'Hello, Vue.js');
}
}
}
</script>
<!-- 父组件,部分代码省略 -->
<template>
<child-component></child-component>
</template>
<script>
export default {
mounted () {
this.$on('test', (text) => {
console.log(text); // Hello, Vue.js
});
}
}
</script>
自行实现
通过目前已知的信息,我们要实现的 dispatch 和 broadcast 方法,将具有以下功能:
- 在子组件调用 dispatch 方法,向上级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该上级组件已预先通过 $on 监听了这个事件;
- 相反,在父组件调用 broadcast 方法,向下级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该下级组件已预先通过 $on 监听了这个事件。 ```bash // 部分代码省略 import Emitter from ‘../mixins/emitter.js’
export default { mixins: [ Emitter ], methods: { handleDispatch () { this.dispatch(); // ① }, handleBroadcast () { this.broadcast(); // ② } } }
emitter.js
```bash
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);
}
}
};
可以看到,在 dispatch 里,通过 while 语句,不断向上遍历更新当前组件(即上下文为当前调用该方法的组件)的父组件实例(变量 parent 即为父组件实例),直到匹配到定义的 componentName 与某个上级组件的 name 选项一致时,结束循环,并在找到的组件实例上,调用 $emit 方法来触发自定义事件 eventName。broadcast 方法与之类似,只不过是向下遍历寻找。
来看一下具体的使用方法。有 A.vue 和 B.vue 两个组件,其中 B 是 A 的子组件,中间可能跨多级,在 A 中向 B 通信:
<!-- A.vue -->
<template>
<button @click="handleClick">触发事件</button>
</template>
<script>
import Emitter from '../mixins/emitter.js';
export default {
name: 'componentA',
mixins: [ Emitter ],
methods: {
handleClick () {
this.broadcast('componentB', 'on-message', 'Hello Vue.js');
}
}
}
</script>
// B.vue
export default {
name: 'componentB',
created () {
this.$on('on-message', this.showMessage);
},
methods: {
showMessage (text) {
window.alert(text);
}
}
}
组件通信3
findComponents 系列方法最终都是返回组件的实例,进而可以读取或调用该组件的数据和方法。
它适用于以下场景:
- 由一个组件,向上找到最近的指定组件;
- 由一个组件,向上找到所有的指定组件;
- 由一个组件,向下找到最近的指定组件;
- 由一个组件,向下找到所有指定的组件;
-
向上找到最近的指定组件——findComponentUpward
// assist.js
// 由一个组件,向上找到最近的指定组件
function findComponentUpward (context, componentName) {
let parent = context.$parent;
let name = parent.$options.name;
while (parent && (!name || [componentName].indexOf(name) < 0)) {
parent = parent.$parent;
if (parent) name = parent.$options.name;
}
return parent;
}
export { findComponentUpward };
向上找到所有的指定组件——findComponentsUpward
// assist.js
// 由一个组件,向上找到所有的指定组件
function findComponentsUpward (context, componentName) {
let parents = [];
const parent = context.$parent;
if (parent) {
if (parent.$options.name === componentName) parents.push(parent);
return parents.concat(findComponentsUpward(parent, componentName));
} else {
return [];
}
}
export { findComponentsUpward };
向下找到最近的指定组件——findComponentDownward
// assist.js
// 由一个组件,向下找到最近的指定组件
function findComponentDownward (context, componentName) {
const childrens = context.$children;
let children = null;
if (childrens.length) {
for (const child of childrens) {
const name = child.$options.name;
if (name === componentName) {
children = child;
break;
} else {
children = findComponentDownward(child, componentName);
if (children) break;
}
}
}
return children;
}
export { findComponentDownward };
向下找到所有指定的组件——findComponentsDownward
// assist.js
// 由一个组件,向下找到所有指定的组件
function findComponentsDownward (context, componentName) {
return context.$children.reduce((components, child) => {
if (child.$options.name === componentName) components.push(child);
const foundChilds = findComponentsDownward(child, componentName);
return components.concat(foundChilds);
}, []);
}
export { findComponentsDownward };
找到指定组件的兄弟组件——findBrothersComponents
// assist.js
// 由一个组件,找到指定组件的兄弟组件
function findBrothersComponents (context, componentName, exceptMe = true) {
let res = context.$parent.$children.filter(item => {
return item.$options.name === componentName;
});
let index = res.findIndex(item => item._uid === context._uid);
if (exceptMe) res.splice(index, 1);
return res;
}
export { findBrothersComponents };
创建组件
创建一个 Vue 实例时,都会有一个选项
el
,来指定实例的根节点,如果不写el
选项,那组件就处于未挂载状态。Vue.extend
的作用,就是基于 Vue 构造器,创建一个“子类”,它的参数跟new Vue
的基本一样,但data
要跟组件一样,是个函数,再配合$mount
,就可以让组件渲染,并且挂载到任意指定的节点上,比如body
。 ```javascript import Vue from ‘vue’;
const AlertComponent = Vue.extend({ template: ‘
这一步,我们调用了 `$mount `方法对组件进行了手动渲染,但它仅仅是被渲染好了,并没有挂载到节点上,也就显示不了组件。此时的 `component `已经是一个标准的 Vue 组件实例,因此它的`$el`属性也可以被访问:
```javascript
const component = new AlertComponent().$mount();
document.body.appendChild(component.$el);
也可以直接挂载
// 在 $mount 里写参数来指定挂载的节点
new AlertComponent().$mount('#app');
// 不用 $mount,直接在创建实例时指定 el 选项
new AlertComponent({ el: '#app' });
也可以直接创建 Vue 实例,并且用一个 Render 函数来渲染一个 .vue 文件
import Vue from 'vue';
import Notification from './notification.vue';
const props = {}; // 这里可以传入一些组件的 props 选项
const Instance = new Vue({
render (h) {
return h(Notification, {
props: props
});
}
});
const component = Instance.$mount();
document.body.appendChild(component.$el);
这样既可以使用 .vue 来写复杂的组件(毕竟在 template 里堆字符串很痛苦),还可以根据需要传入适当的 props。渲染后,如果想操作 Render 的 Notification
实例,也是很简单的:
const notification = Instance.$children[0];
因为 Instance 下只 Render 了 Notification 一个子组件,所以可以用 $children[0]
访问到。
需要注意的是,我们是用 $mount
手动渲染的组件,如果要销毁,也要用 $destroy
来手动销毁实例,必要时,也可以用 removeChild
把节点从 DOM 中移除。