推荐我的vue教程:VUE系列教程目录
上篇讲解了vue-router路由入门,现在讲讲关于vue组件的内容。
如果你们有使用过element组件的话,他就是以vue组件的形式进行封装的,在讲解组件之前我们需要知道vue是数据驱动的,它的一切依赖于数据,我们应该根据数据的不同来进行相关的处理,在这一前提下才能形成vue框架的思考模式。
在了解这一模式的前提下我们来看看vue组件是个什么东西。
什么是VUE组件?
在github上,各位请使用git拉一下项目:vuetemplate。不会使用git拉文件的请去GitHub上下载压缩包。
在/src/page/components
下是有关组件的代码
我们打开vue官网的组件API,可以简单浏览,对于新手来说这个API的阅读有时很晦涩,或者跟实际应用有些许差别。于是我的讲解是建立在对这个API的补充说明与简单化的,所以你们最好还是看看这个。
VUE组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,VUE组件是自定义元素, Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以是原生 HTML 元素的形式,以 is 特性扩展。
注册组件
在注册组件时我们有两种方式,第一种注册全局组件,第二种是局部使用组件,对于那些应用多的组件来说全局无疑是最好的选择,对于变数太大,应用不多且在统一目录下的局部使用是我们想要的。
全局注册:
要注册一个全局组件,你可以使用 Vue.component(tagName, options)。
// 模板
Vue.component('my-component', {
// 选项
})
一个模板并不能说明什么,实例才能让人看的更明白:
// html
<my-component></my-component>
// 注册
Vue.component('my-component', {
template: '<div>A custom component!</div>'
})
组件的简单使用就是这个样子,即<my-component></my-component>
最后变成了<div>A custom component!</div>
。可是,实际操作中我们并不会这么弱智的使用,组件的复杂度远远不是这个样子的。
局部注册
要注册一个局部组件,你可以使用 Vue的components属性。
<script>
export default {
components: {},
data () {
return {
}
}
}
</script>
例子才是真理:
// html
<my-component></my-component>
<script>
var vuecomponent = {
template: '<div>A custom component!</div>'
}
export default {
components: {
'my-component': vuecomponent
},
data () {
return {
}
}
}
</script>
DOM渲染的局限
在html中我们知道有些标签的孩子是固定的比如<ul> ,<ol>,<table> ,<select>
限制了能被它包裹的元素,例如ul里面只能包裹li。同时,一些像 <option> 这样的元素只能出现在某些其它元素内部。
在自定义组件中使用这些受限制的元素时会导致一些问题,例如:
<table>
<my-row>...</my-row>
</table>
自定义组件 <my-row> 被认为是无效的内容,因此在渲染的时候会导致错误。那我们怎么解决呢?变通的方案是使用特殊的 is 属性:
<table>
<tr is="my-row"></tr>
</table>
data 必须是函数
组件中必须是函数,通过Vue构造器传入的各种选项大多数都可以在组件里用。 data 是一个例外,它必须是函数。
// html
<simple-counter></simple-counter>
<script>
export default {
components: {
'simple-counter': {
template: '<el-button size="small" v-on:click="counter += 1">{{ counter }}</el-button>',
// 技术上 data 的确是一个函数了,因此 Vue 不会警告,
// 但是我们返回给每个组件的实例的却引用了同一个data对象
data: function () {
return {
counter: 0
}
}
}
},
data () {
return {
}
}
}
</script>
这里有很多人看蒙了,问起初不是这样写吗???
data: {
counter: 0
}
这里别问,我起初也没看懂,你可以这样想:把所有关于data的数据引入变为data () {}
就可以了。
为何我会这样说?我曾经在上一篇文章里说:vue路由的本质是根据url的不同来进行组件的各种切换罢了。而组件的data必须是数据,所以你看我写的代码里关于.vue
文件的都使用的是这种结构:
<template>
<div>
</div>
</template>
<script>
export default {
data () {
return {
}
}
}
</script>
如果不明白你可以看上面的<simple-counter>
组件的例子,其最终的本质变成了:
<template>
<div>
<el-button size="small" v-on:click="counter += 1">{{ counter }}</el-button>
</div>
</template>
<script>
export default {
data () {
return {
counter: 0
}
}
}
</script>
其实你只要使用vue-router路由你的.vue
文件的结构只能变成这样:
<template>
<div>
</div>
</template>
<script>
export default {
data () {
return {
// 这里写基础数据
}
}
}
</script>
构成组件
组件意味着协同工作,引用组件的地方叫父组件,如<simple-counter></simple-counter>
,组件的内容则被成为子组件。
通常父子组件会是这样的关系:组件 A 在它的模版中使用了组件 B 。它们之间必然需要相互通信:父组件要给子组件传递数据,子组件需要将它内部发生的事情告知给父组件。然而,在一个良好定义的接口中尽可能将父子组件解耦是很重要的。这保证了每个组件可以在相对隔离的环境中书写和理解,也大幅提高了组件的可维护性和可重用性。
在 Vue.js 中,父子组件的关系可以总结为 props down, events up 。父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息。看看它们是怎么工作的?如图:
父子组件的交互的原理这个具体呢?
父子组件的交互
props的单向流
组件实例的作用域是孤立的。这意味着不能(也不应该)在子组件的模板内直接引用父组件的数据。要让子组件使用父组件的数据,我们需要通过子组件的props选项。
我们在开发中,一个单文件组件的.vue
文件的基本构成是这样的:
<template>
<div>
</div>
</template>
<script>
export default {
// el: '',
props: {}, // 父到子传参
components: {}, // 组件接收
mixins: [], // 混合
data () { // 基础数据
return {
//
}
},
created () {}, // 创建周期
watch: {}, // 状态过渡
methods: {}, // 方法存放的地方
computed: {}, // 计算属性存放的地方
filters: {}, // 过滤
directives: {} // 指令
}
</script>
可是常用的很少,父组件给子组件传值使用的就是props选项
。
props选项可以接受两种模式的参数:
第一种固定的属性:如这样
message="hello!
传一个普通的字符;
第二种动态属性:如这样v-bind:myMessage="this.message"
传一个变量,其可以简化为:myMessage="this.message"
例子如下:(父子组件在同一目录下,子组件-child.vue)
// 父组件
// HTML
<child message="HELLO!" :my-message="this.message"></child>
// script
<script>
import child from './child.vue'
export default {
components: {
child: child
}
},
data () {
return {
message: '你猜'
}
}
}
</script>
// 子组件
<template>
<div>
<div>{{message}}</div>
<div v-text="myMessage"></div>
</div>
</template>
<script>
export default {
props: {
message: null,
myMessage: null
}, // 父到子传参
data () { // 基础数据
return {
//
}
},
created () {}, // 创建周期
watch: {}, // 状态过渡
methods: {} // 方法存放的地方
}
</script>
prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是不会反过来。这是为了防止子组件无意修改了父组件的状态。
另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop 。如果你这么做了,Vue 会在控制台给出警告。
但是有时我们就是需要修改,怎么办?(老子需求)
一般情况下我们修改通常是这两种原因:
- prop 作为初始值传入后,子组件想把它当作局部数据来用;
- prop 作为初始值传入,由子组件处理成其它数据输出。
对于第一种情况我们可以李代桃僵,即用另外一个变量替代它,把它的值赋给那个变量:
data () { // 基础数据
return {
//
counter: this.message
}
}
第二种情况可以定义一个计算属性,处理 prop 的值并返回:
computed: {
messagetoLowerCase: function () {
return this.message.trim().toLowerCase()
}
}
注意:使用字面量语法传递数值时,必须使用动态props,即如这样`v-bind:number="1"`
props验证
我们可以为组件的 props 指定验证规格。如果传入的数据不符合规格,Vue 会发出警告。当组件给其他人使用时,这很有用。
要指定验证规格,需要用对象的形式,而不能用字符串数组:(修改上面的例子)
// html
<child message="HELLO!" :my-message="this.message" :number="11"></child>
// 子组件props
props: {
message: String,
myMessage: {
type: String,
required: true
},
number: {
validator: function (value) {
return value > 10
}
}
}
验证规格模板:
props: {
// 基础类型检测 (`null` 意思是任何类型都可以)
propA: Number,
// 多种类型
propB: [String, Number],
// 必传且是字符串
propC: {
type: String,
required: true
},
// 数字,有默认值,如果你没有传则以默认为准
propD: {
type: Number,
default: 100
},
// 数组或对象的默认值应当由一个工厂函数返回
propE: {
type: Object,
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator: function (value) {
return value > 10
}
}
}
自定义事件向父组件传值
事件$on与$emit
我们知道,父组件是使用 props 传递数据给子组件,但如果子组件要把数据传递回去,应该怎样做?那就是自定义事件!
每个 Vue 实例都实现了事件接口(Events interface),即:
使用 $on(eventName) 监听事件
使用 $emit(eventName) 触发事件
注意:Vue的事件系统分离自浏览器的EventTarget API。尽管它们的运行类似,但是$on 和 $emit 不是addEventListener 和 dispatchEvent 的别名。
你们一定很奇怪怎么用事件监听来向父元素传递?
其实原理很简单就是我们在父组件上通过v-on
监听子组件的事件,而子组件通过$emit(eventName) 触发事件。例子如下:
// 父组件
<template>
<div>
<child v-on:onchild="inparent"></child>
</div>
</template>
<script>
import child from './child.vue'
export default {
components: {
child: child
},
data () {
return {
}
},
methods: {
inparent () {
alert('父组件响应了')
}
}
}
</script>
// 子组件
<template>
<div>
<el-button size="small" v-on:click="onparent">父组件响应吧!!!</el-button>
</div>
</template>
<script>
export default {
props: {},
data () { // 基础数据
return {
}
},
methods: {
onparent () {
this.$emit('onchild')
}
}
}
</script>
这个例子中,子组件给父组件传值通过$emit('onchild')
,触发父组件的v-on:onchild
,v-on:onchild
响应后执行inparent
函数。但是就达到我们的目的了???
传值,传值,传值,值呢?这个API里可没说,那怎么办呢?
很简单按照程序工程师的思路来想,值肯定是这种模式:
this.$emit('onchild', 需要的值)
// 多个呢?
this.$emit('onchild', 需要的值1,需要的值2)
那接值呢?
onparent (需要的值1, 需要的值2) {
}
所以完整的模式应该是这样的:
// 父组件
<template>
<div>
<child v-on:onchild="inparent"></child>
</div>
</template>
<script>
import child from './child.vue'
export default {
components: {
child: child
},
data () {
return {
}
},
methods: {
inparent (childs, childrens) {
alert('父组件响应了')
console.log(childs)
console.log(childrens)
}
}
}
</script>
// 子组件
<template>
<div>
<el-button size="small" v-on:click="onparent">父组件响应吧!!!</el-button>
</div>
</template>
<script>
export default {
props: {},
data () { // 基础数据
return {
childs: '我是孩子的值',
childrens: '我是孩子的另一个值'
}
},
methods: {
onparent () {
this.$emit('onchild', this.childs, this.childrens)
}
}
}
</script>
sync-修饰符
在一些情况下,我们可能会需要对一个 prop 进行『双向绑定』。当一个子组件改变了一个 prop 的值时,这个变化也会同步到父组件中所绑定的值。这很方便,但也会导致问题,因为它破坏了『单向数据流』的假设。由于子组件改变 prop 的代码和普通的状态改动代码毫无区别,当光看子组件的代码时,你完全不知道它何时悄悄地改变了父组件的状态。这在 debug 复杂结构的应用时会带来很高的维护成本。
事实上,这正是 Vue 1.x 中的 .sync修饰符所提供的功能。但是VUE在 2.0 中移除了 .sync。后来在 2.3 VUE又重新引入了 .sync 修饰符。
// 父组件
<template>
<div>
<child :foo.sync="bar"></child>
</div>
</template>
<script>
import child from './child.vue'
export default {
components: {
child: child
},
data () {
return {
bar: 1
}
},
watch: {
bar: function () {
console.log(this.bar)
}
}
}
</script>
// 子组件
<template>
<div>
<div>{{foo}}</div>
<el-button size="small" v-on:click="onsync">改变foo</el-button>
</div>
</template>
<script>
export default {
props: {
foo: null
},
data () { // 基础数据
return {}
},
methods: {
onsync () {
this.$emit('update:foo', this.foo + 1)
}
}
}
</script>
其实本质上VUE做到的只是:需要做的只是让子组件改变父组件状态的代码更容易被区分。
即把<comp :foo="bar" @update:foo="val => bar = val"></comp>
简写为<child :foo.sync="bar"></child>
,不让使用者在父元素上进行事件监听了而已其他都是一样的,它通过子组件传值改变父组件,依赖props传值把修改的父组件元素再传回子组件而已。
小结:
父组件向子组件传值通过props;子组件向父组件传值,我们在父组件上通过v-on
监听子组件的事件,而子组件通过$emit(eventName) 触发事件。
至此组件的基本知识就结束了,高深的组件有关的,下一节再说。
提示:在最近几天我会慢慢附上VUE系列教程的其他后续篇幅,后面还有精彩敬请期待,请大家关注我的专题:web前端。如有意见可以进行评论,每一条评论我都会认真对待。