[TOC]
什么是组件化?
- 如果我们将一个页面中的所有处理逻辑都放在一起,处理起来就会非常麻烦,不利于后续的管理和扩展
- 但是,如果我们将一个页面拆分为一个个小的功能模块,每个模块完成自己这一部分的功能,那么之后整个页面的管理和维护就会变得非常容易了
Vue中提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构建我们的应用,任何应用都可以被抽象成一棵组件树
全局组件和局部组件
全局组件:在Vue实例外部声明的组件(可全局使用)
- 局部组件:在Vue实例内部声明的组件,(只能在该组件绑定的div元素内部使用)
父组件和子组件
- 组件的基本使用过程:创建组件构造器(Vue.extends()方法)>>>>>>>>>>>>>注册组件(Vue.component()方法)>>>>>>>>>>>>>使用组件(在Vue实例的作用域范围内使用组件)
所谓父子组件就是一个组件中使用了另一个组件,如下所示: ```html
- 父子组件的错误使用:已子标签的形式在Vue实例中直接使用 - 因为当子组件被注册到父组件的components中时,Vue已经编译好了父组件的模板,该模板的内容已经决定了父组件将要渲染的HTML(相当于父组件中已经有了子组件中的内容了),其实就是在编译的时候就会对我们注册的组件进行替换 - <cpn1></cpn1>只能在组件2(也就是父组件)中被识别 <a name="GLbAM"></a> ## 注册组件的语法糖写法 - 用的较多 ```html <script> // 全局注册组件的语法糖 Vue.component('cpn1', { template: ` <div> <h2>111111</h2> <p></p> </div> ` }) // 局部注册组件的语法糖 const app = new Vue({ el: '#app', data: { message: 'hello' }, component: { 'cpn2': { template: ` <div> <h2>111111</h2> <p></p> </div> ` } } }) </script>组件模板分离写法
<template id="cpn"> <div> <h2>11111</h2> <p>22222</p> </div> </template> <script> Vue.component('cpn', { template: '#cpn' }) </script>为什么组件data必须是函数
- 组件是一个单独功能模块的封装,这个模块有属于自己的html模板,也应该有属于自己的数据data
- 组件中的数据是保存在哪里的呢?是在顶层的Vue实例中吗?经过测试,我们组件是不可以直接访问Vue实例中的data的,而且即使可以访问,如果将所有的数据都放在Vue实例中,Vue实例就会变得非常臃肿
- 结论:Vue组件应该有自己保存数据的地方
vue.component()方法中传入这个对象{ }不仅有template属性,也有data属性(也可以有methods属性),这里存放我们组件自己的数据,但是:这个data属性不能是对象类型,必须是个函数,这个函数返回一个对象,对象内部保存着数据
Vue.component('cpn', { template: '#cpn', data () { return { title: 'abc' } } })封装一个计数器组件来说明这个问题 ```html
当前计数:{{counter}}- 由于我们的组件要使用多次,如果在注册组件时,data是一个对象(真实这样会报错,假如可以使用),那么多次使用该组件时,每个组件实例使用的都是同一个对象,这样任何一个组件实例修改了data中的数据,所有的组件实例都会发生改变,这就会造成组件实例之间的互相影响,这是我们竭力想要避免的情况 - 但是如果data是一个函数,每个组件实例使用的data对象,都是由这个函数来返回的,这样的话,每个组件实例使用的对象都是各不相同的(内存地址不一样),这些对象之间互不影响,可以理解为每个组件实例的私有对象 - 但是:如果我们想让这些组件实例之间互相影响,我们就可以定义一个对象,然后让data函数返回这个对象,在实际开发中,千万不要使用这种方式,因为组件是要复用的,每个组件都有属于自己的逻辑,不能引起连锁反应! ```javascript const obj = { counter: 0 } Vue.component('cpn', { template: '#cpn', data () { return obj } })- 总结:组件应该在每个地方(组件实例)都有一份属于自己的保存数据的对象
组件间通信
- 之前我们说过,子组件是不能使用父组件或者Vue实例的数据的
- 但是在开发中,往往一些数据确实需要从上层传递到下层,比如在一个页面中,我们从服务器请求到了很多的数据,其中一部分数据并不是我们整个页面的大组件来展示的,而是需要下面的子组件进行展示,这个时候,我们并不会为了一个小数据重新发送一个网络请求,而是直接让大组件(父组件)将数据传递给小组件(子组件)
- 如何进行父子组件之间的通信呢?
- 父组件通过props向子组件传递数据
- 子组件通过事件向父组件发送消息emit Events
父传子(props)
<div id="app"> <!-- 3. 使用组件 --> <cpn v-bind:imovies="movies" :imessage="message"></cpn> <cpn v-bind:imovies="movies"></cpn> <cpn v-bind:imovies="movies"></cpn> <cpn v-bind:imovies="movies"></cpn> </div> <template id="cpn"> <div> <ul> <li v-for="(item, index) in imovies">{{item}}</li> </ul> </div> </template> <script> // 1. 创建一个组件构造器 const cpn = { template: '#cpn', props: { imovies: { type: Array, // 如果属性的类型是对象或者数组时,默认值必须是一个函数,此时如果是一个数据就会报错 default () { return [] }, required: true,// 必需,当我们使用此cpn组件时,必须传递这个值,否则会报错 }, imessage: { type: String, default: 'hello world!' } }, data () { return { } }, methods: {} } // Vue实例,也就是父组件 const app = new Vue({ el: '#app', data: { message: 'hello', movies: ['real Steal','iron man','transformers','into the wild'] }, components: { // 2. 注册组件,增强写法 cpn } }) </script>props的几种使用
Vue.component('cpn', { props: { // 基础的类型检查 propsA: Number, // 多个可能的类型 propsB: [String, Number], // 必传的字符串 propsC: { type: String, required: true }, // 带有默认值的数字 propsD: { type: Number, default: 100 }, // 带有默认值的对象 propsE: { type: Object, default () { return {} } }, // 自定义验证函数 propsF: { validator () { // 这个值必须匹配下列字符串中的一个 return ['success','warning','danger'].indexOf(value) !== -1 } } // 可以自定义类型 } })props驼峰标识:我们在使用v-bind时,被绑定的属性名不能有大写,如果变量名使用了驼峰,在标签中进行绑定时,要将驼峰改为-分割来使用
子传父(自定义事件)
<div id="app"> <!-- 以前v-on:后面跟的都是一些默认的事件,比如click input hover事件等,现在这里跟的是我们自定义的事件item-click --> <!-- 我们不仅可以监听默认事件,而且还可以监听我们子组件发射(emit)出来的事件 --> <!-- 我们这里监听到子组件传递过来的事件后,通过cpnClick函数来进行处理 --> <!-- 如果这里的cpnClick没有跟(参数),就默认把this.$emit('item-click', item)这里的item传递过去。而之前js中监听事件默认传递的是浏览器的事件对象event --> <cpn v-on:item-click="cpnClick"></cpn> </div> <template id="cpn"> <div> <button v-for="item in categories" @click="btnClick(item)">{{item.name}}</button> </div> </template> <script> // 1. 子组件 const cpn = { template: '#cpn', data () { return { categories: [ {id:'aaa', name:'热门推荐'}, {id:'bbb', name:'手机数码'}, {id:'ccc', name:'家用家电'}, {id:'ddd', name:'电脑办公'} ] } }, methods: { btnClick (item) { // 子组件发送出去一个自定义事件 this.$emit('item-click', item) } } } const app = new Vue({ el: '#app', data: { }, components: { cpn }, methods: { cpnClick (item) { console.log(item.name) } } }) </script>父子组件通信案例
- 现在要实现的功能是这样的:子组件的num1和num2都是在props中的,也就是由父组件传递过来的值,但是我们又想要在模板中加input标签,让input的输入值和子组件中的num1和num2进行双向绑定(也就是修改input的值,子组件props中的num1和num2也跟着变化),同时把该值传回父组件,并修改父组件中的data的值
- 现在的问题就是:num1 和 num2 由父组件传递过来的值决定,也被input输入的值来决定,Vue是不允许我们这样做的,因为可能会乱套,不知道这个值到底是被谁决定的
```html
{{num1}}
{{num2}}
- 但是如果我们确实需要将子组件props中的值和input中的值进行双向绑定,要怎么办呢? - Vue希望我们根据props中的值,创建一个cpn组件自己的data或者计算属性 - 然后input想要绑定的话就绑定cpn组件内部的data属性中的值,这样就不会影响父组件传递过来的值了 ```javascript cpn: { template: '#cpn', props: { num1: Number, num2: Number }, data () { return { dnum1: this.num1, dnum2: this.num2 } } }- 接下来我们还要实现将该值传递回父组件中,并修改父组件中的data的值
- 要实现该功能,我们就不能使用v-model来双向绑定了,我们知道,v-model是由两个指令构成的,v-bind和@input,且用这种方式,我们可以自定义获取输入框焦点之后的操作,更加灵活
```html
- 要实现该功能,我们就不能使用v-model来双向绑定了,我们知道,v-model是由两个指令构成的,v-bind和@input,且用这种方式,我们可以自定义获取输入框焦点之后的操作,更加灵活
```html
{{dnum1}}_{{num1}}
{{dnum2}}_{{num2}}
<a name="UsRgN"></a> ### watch - watch属性用于监听某一个属性的变化,属性名作为函数名称,两个参数,oldVaule和newValue ```javascript watch: { name (oldValue, newValue) { 这时就可以对这两个进行处理 } }父子组件如何拿到对方的对象直接进行操作
父访问子($children/$refs)
<div id="app"> <cpn></cpn> <cpn ref="aaa"></cpn> <button @click="btnClick"> 按钮 </button> </div> <template id="cpn"> <div> <h2>我是子组件</h2> </div> </template> <script> const app = new Vue({ el: '#app', data: { message: 'hello' }, methods: { btnClick () { // 方法1: $children console.log(this.$children); // VueComponent console.log(this.$children[0].name); // ryan z this.$children[0].showMessage(); // alert(111) // 方法2: $refs,用得最多99% console.log(this.$refs); //默认为一个空对象{} // 如果我们需要使用这个refs,就需要在组件中添加一个属性ref="aaa",相当于起了个名字,这时就可以通过下面的方式来使用了 console.log(this.$refs.aaa); // {aaa:VueComponent} 这样就可以拿到这个组件对象 } }, components: { cpn: { template: '#cpn', data () { return { name: 'ryan z' } }, methods: { showMessage () { alert(111) } } } } }) </script>子访问父($parent)+ 访问根组件($root)
- 一般开发中这种用法很少,因为如果子组件频繁使用父组件中对象的属性和方法,它自身的复用性就不会很强了,而我们封装子组件的目的就是它的复用性
- $root一般使用也很少,根组件就是Vue实例,而且我们Vue实例中也不会放很多数据(除了路由等一些比较重要的)
```html
子组件
```
