组件编写原则
- data 必须是一个函数(复用时有分开的作用域)
- 单个根元素
- 组件名称:
- ‘my-component-name’:使用时只能是
<my-component-name>
- ’MyComponentName’:使用时可以
<my-component-name>
和<MyComponentName>
- ‘my-component-name’:使用时只能是
组件注册
全局
可以在 Vue 根实例下的任何地方使用
Vue.component('my-cpn', {
template: `
<div>
<h3>我是全局</h3>
</div>
`
})
局部
var myapp = new Vue({
el: '#myapp',
components: {
'my-cpn': {
template: `
<div>
<h3>我是局部</h3>
</div>
`
}
}
})
组件模板分离写法
方法1. script 标签:
<script type="text/x-template" id="cpn1">
<div>
<h3>组件模板分离 script</h3>
</div>
</script>
方法2. template 标签:
<template id="cpn2">
<div>
<h3>组件模板分离 template</h3>
</div>
</template>
注册:
Vue.component('my-cpn1', {
template: '#cpn1' // template 对应上面标签的 id
})
Vue.component('my-cpn2', {
template: '#cpn2' // template 对应上面标签的 id
})
使用 Vue.component
来定义组件有很多缺点:
- 全局定义强制要求每个 component 中的命名不得重复
- 字符串模板缺乏语法高亮
- 不支持 CSS
- 不能使用预处理器
所以使用 .vue
文件扩展名的单文件组件:
- CommonJS 模块
- 完整语法高亮
- 组件作用域的 CSS
- 可以使用预处理器
父子组件通信
父传子:props
// 在标签中动态绑定 v-bind:子组件中props定义的变量="父组件中对应的变量"
<div id="myapp">
<cpn v-bind:parentmsg="message"></cpn>
</div>
// 在子组件模板中使用props定义的变量
<template id="cpn">
<p>我是子组件,接收到{{ parentmsg }}</p>
</template>
<script>
const cpn = {
template: '#cpn',
props: ['parentmsg'] // 父组件传递过来的数据,子组件只读
data() {
return {
mydata: '子组件的值' // 子组件私有的数据,子组件可读可写
}
},
}
var myapp = new Vue({
el: '#myapp',
data: {
message: '父组件的值'
},
// 子组件注册
components: {
cpn // 对象属性简写,左右两边相同时的简写,等同写法 cpn: cpn
}
})
</script>
特别地:v-bind=“obj” 等同于把 obj 对象里的所有属性传进去
子组件中属性 props 的类型:
- 字符串数组形式:
props: ['para1', 'para2', 'para3']
- 对象形式:(一般用对象)
props: {
// 指定类型,有 String, Number, Boolean, Array, Object, Function, Date, Symbol
para1: String,
para2: [String, Number], // 多个可能的类型
para3: {
type: String,
required: true // required表示必须传值
}
para4: {
type: Array,
// 类型是对象或数组 Object/Array 时,默认值必须是一个函数
default: function() {
return []
}
}
}
注意:
- prop 命名在 JS 中是小驼峰(camelCase),HTML 中是短横线(kebab-case)
- 父子组件之间是单向下行绑定,子组件不要改变父组件的 props
- 每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值
- 如果需要子组件中的值同步 props 的值更新,用计算属性
- 如果传入一个参数,但是子组件中没有定义,那么会自动绑定到子组件的根元素上。可以在子组件中使用
inheritAttrs: false
来防止自动绑定(除了 class 和 style) this.$attrs
表示组件中没有定义的传入的 props
子传父:$emit
<div id="myapp">
{{ message }}
<!-- 自定义事件my-event,触发时调用父组件的cpnclick方法 -->
<cpn v-on:my-event="cpnclick"></cpn>
</div>
<template id="cpn">
<!-- 子组件监听点击事件触发myclick -->
<p v-on:click="myclick('abc')">我是子组件,点我传值 abc 给父组件</p>
</template>
const cpn = {
template: '#cpn',
methods: {
// 监听到事件发生时,发送自定义事件给父组件,value 是'abc'
myclick(value) {
this.$emit('my-event', value)
}
}
}
var myapp = new Vue({
el: '#myapp',
data: {
message: '父组件的值'
},
// 子组件
components: {
cpn // 对象属性简写,左右两边相同时的简写,等同写法 cpn: cpn
},
methods: {
cpnclick(value) {
this.message = '接收到子组件的值:' + value
}
}
})
注意:
- 自定义事件不存在任何自动化的大小写转换,始终使用短横线(kebab-case)的事件名
this.$listeners
表示组件上绑定的所有事件- 父子组件的 prop 如果需要“双向绑定”进行同步更新,正确的做法是在子组件中 emit 一个自定义事件,同时有一个 .sync 语法糖:
<my-cpn
v-bind:title="title"
v-on:update:title="title = $event"
></my-cpn>
this.$emit('update:title', newTitle) // 子组件中修改值
// 此情况下,等同于:
<my-cpn v-bind:title.sync="title"></my-cpn>
this.$emit('update:title', newTitle)
父组件监听子组件自定义事件时,在接受子组件参数的同时传入自定义参数:
// 子组件:
this.$emit('custom', value1, value2, ...)
// 父组件:
<child @custom="parentMethod(...arguments, newValue)"></child>
// arguments 就是子组件传出来的值
父访问子:$refs
或者 $children
一般用 $refs
,要访问所有子组件时会用 $children
<!-- 父组件 -->
<div id="myapp">
<cpn></cpn>
<!-- 给这个子组件命名为aaa -->
<cpn ref="aaa"></cpn>
<button @click="btnClick">按钮</button>
</div>
<!-- 子组件 -->
<template id="cpn">
<div>我是子组件</div>
</template>
<script>
// 父组件
const myapp = new Vue({
el: '#myapp',
data: {
message: '父组件的值'
},
methods: {
btnClick() {
// 1.第一种方法:$children
// 打印出子组件
console.log(this.$children);
// 调用[第一个]子组件的showMessage方法
this.$children[0].showMessage();
// 2.第二种方法:$refs
// 打印出名字为aaa的子组件里的值
console.log(this.$refs.aaa.message);
}
},
// 子组件
components: {
cpn: {
template: '#cpn',
data() {
return {
message: '子组件的值'
}
},
methods: {
showMessage() {
console.log('子组件的showMessage()被调用');
}
}
}
},
})
</script>
$refs
只会在组件渲染完成之后生效,并且它们不是响应式的
$children
并不保证顺序,也不是响应式的
子访问父和根:$parent
或者 $root
一般不用,因为会导致父子组件耦合度变大,出了问题很难调试发现是哪里变更了数据
$parent
:父实例$root
:根实例
父组件监听子组件生命周期事件
在子组件上监听 hook 事件:
// 子组件
<child @hook:mounted="getDom"></child>
// 父组件
methods:{
getDom() {
// 获取子元素等操作
}
}
Vue官方文档里没告诉你的神秘钩子——@hook一个关于“如何实现父组件监听子组件生命周期“引发的vue开发小技巧;揭秘 - 掘金
父子组件传递引用值
跨层级祖孙通信
1. provide
和 inject
provide 和 inject 绑定并不是可响应的,如果要想 inject 接受的变量是响应式的,provide 提供的变量本身就需要是响应式的
// 父级组件提供 'foo'
var Provider = {
provide: {
foo: 'bar'
},
// ...
}
// 子组件注入 'foo'
var Child = {
inject: ['foo'],
created () {
console.log(this.foo) // => "bar"
}
// ...
}
2. $attrs
和 $listeners
仅仅用来中间传递数据而不做处理,即数据透传
实现:中间组件在其子组件上加上 v-bind="$attrs"
和 v-on="$listeners"
,将它的父组件的数据传向它的子组件
其中:$attrs
是没有在 props 中定义的,$listeners
是绑定的所有事件
其他组件通信方式
vuex
全局状态管理,适用于多个组件共同维护数据,且对数据有一定的处理
EventBus
原理是 发布订阅模式 结合
$emit
$on
$off
1.定义一个事件中心:实质上 EventBus 是一个不具备 DOM 的 vue 实例
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
2.组件 A 发送一个事件
<script>
import { EventBus } from "./event-bus.js"
export default {
methods: {
sendMsg() {
EventBus.$emit("msg", '来自A页面的消息');
}
}
};
</script>
3.组件 B 中接受 A 的事件
<script>
import { EventBus } from "./event-bus.js"
export default {
mounted() {
EventBus.$on('msg', msg => {
// ...
})
}
};
</script>
4.移除事件监听
EventBus.$off('msg')
localStorage / sessionStorage
注意用 JSON.parse()
/ JSON.stringify()
做数据格式转换
可以和 vuex 结合进行数据持久化保存
组件通信总结
方式 | 优点 | 缺点 |
---|---|---|
prop ,$emit | 常用 | 只能父子 |
$refs ,$children | 偶尔用 | 父子组件耦合 |
$parent ,$root | 基本不用 | 父子组件严重耦合 |
provide ,inject | 跨层级传数据方便 | 不清楚数据来源与变更 |
$attrs ,$listeners | 数据透传 | 中间组件多余代码 |
vuex | 数据集中管理,可追溯 | 代码比较复杂 |
EventBus | 各种不同组件间通信很方便,代码简洁 | 事件多了后,难以对事件进行维护 |
localStorage sessionStorage | 简单,浏览器自带 | 数据和状态比较混乱,不容易维护 |
动态组件
在 component 内置组件上使用 is 来切换不同的组件:<component v-bind:is="xxx"></component>
如果在切换的时候想保存之前组件的状态:使用 keep-alive
<keep-alive>
<component v-bind:is="xxx"></component>
</keep-alive>
异步组件
当需要用到组件时才去加载
使用 import 动态加载:() => import('./my-async-component')