认识组件化
如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。
但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。
组件化是Vue.js中的重要思想。它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。任何的应用都会被抽象成一颗组件树。
注册组件
组件的使用分成三个步骤:创建组件构造器(在vue2.x中取消)、注册组件、使用组件。
组件其他补充
全局组件和局部组件
当我们通过调用Vue.component()
注册组件时,组件的注册是全局的。这意味着该组件可以在任意Vue示例下使用。
<div id="app1">
<my-btn></my-btn>
</div>
/* 两个都被渲染出来 */
<div id="app2">
<my-btn></my-btn>
</div>
<script src="../js/vue.js"></script>
<script>
const cpn = Vue.extend({
template: `<div>哈哈哈</div>`
});
Vue.component("my-btn", cpn);
const app1 = new Vue({
el: "#app1",
data: {},
});
const app2 = new Vue({
el: "#app2",
data: {}
})
</script>
//哈哈哈
//哈哈哈
如果我们注册的组件是挂载在某个实例中, 那么就是一个局部组件。
<div id="app1">
<my-btn></my-btn>
</div>
/*app2 没有被渲染出来*/
<div id="app2">
<my-btn></my-btn>
</div>
<script src="../js/vue.js"></script>
<script>
const cpn = Vue.extend({
template: `<p>哈哈哈</p>`
});
const app1 = new Vue({
el: "#app1",
data: {},
components: {
"my-btn": cpn,
}
});
const app2 = new Vue({
el: "#app2",
data: {}
})
</script>
父组件和子组件
组件和组件之间存在层级关系。而其中一种非常重要的关系就是父子组件的关系。
<div id="app">
<cpnc1></cpnc1>
</div>
<script src="../js/vue.js"></script>
<script>
const cpn2 = Vue.extend({
template: `
<p>我是cpn2组件</p>
`
});
const cpn1 = Vue.extend({
template: `
<div id="app1">
<h2>我是cpn1组件</h2>
<cpnc2></cpnc2>
</div>
`,
components: {
cpnc2: cpn2,
}
});
const app = new Vue({
el: "#app",
data: {
message: "hello vue"
},
methods: {},
components: {
cpnc1: cpn1,
}
})
</script>
注意:无法在Vue实例中直接使用子组件cpn2。
注册组件语法糖
在上面注册组件的方式,可能会有些繁琐。Vue为了简化这个过程,提供了注册的语法糖。主要是省去了调用Vue.extend()的步骤,而是可以直接使用一个对象来代替。
<body>
<div id="app">
<cpn1></cpn1>
<cpn2></cpn2>
</div>
<script src="../js/vue.js"></script>
<script>
// 2.注册组件
Vue.component('cpn1', {
template: `
<div>
<h2>我是标题1</h2>
<p>我是内容, 哈哈哈哈</p>
</div>`
})
// 2.注册局部组件的语法糖
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
'cpn2': {
template: `
<div>
<h2>我是标题2</h2>
<p>我是内容, 呵呵呵</p>
</div>`
}
}
})
</script>
</body>
模板的分离写法
通过语法糖简化了Vue组件的注册过程,另外还有一个地方的写法比较麻烦,就是template模块写法。
Vue提供了两种方案来定义HTML模块内容:使用<script>
标签、使用<template>
标签
<div id="app">
<cpn1></cpn1>
</div>
<!--1. script标签-->
<script type="text/x-template" id="cpn1">
<div>
<h2>cpn1</h2>
</div>
</script>
<!--2. template-->
<template id="cpn1">
<div>
<h2>cpn1</h2>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
message: "hello vue"
},
methods: {},
components: {
"cpn1": {
template: "#cpn1"
}
}
})
</script>
组件数据存放
组件是一个单独功能模块的封装:这个模块有属于自己的HTML模板,也应该有属性自己的数据data。组件对象也有一个data属性,这个data属性必须是一个函数,而且这个函数返回一个对象,对象内部保存着数据。
为什么data在组件中必须是一个函数?
首先,如果不是一个函数,Vue直接就会报错。
其次,原因是在于Vue让每个组件对象都返回一个新的对象,因为如果是同一个对象的,组件在多次使用后会相互影响。返回函数,目的是让每一个组件都有一个属于自己的状态。
在根组件data不需要为函数是因为根组件只有一个。
说明代码:
//示例1
function foo() {
return {
name: "tom",
age: 22
}
}
let obj1 = foo();
let obj2 = foo();
obj1.name = 'Jack';
console.log(obj2.name);//tom
//示例2
let obj = {
name: "tom",
age: 22
};
function bar() {
return obj;
}
let obj1 = bar();
let obj2 = bar();
obj1.name = "Jack";
console.log(obj2.name);//Jack
父子组件通信
子组件是不能引用父组件或者Vue实例的数据的。如何进行父子组件间的通信呢?Vue官方提到:
在父组件中通过props向子组件传递数据;
在子组件中通过事件向父组件发送消息。
父级向子级传递
在子组件中,使用选项props
来声明需要从父级接收到的数据。
因为v-bind不支持驼峰标识,如果在子组件props使用驼峰标识(cMess),那么在使用子组件的绑定时应使用(c-mess)。
props的值有两种方式:
方式一:字符串数组,数组中的字符串就是传递时的名称。
方式二:对象,对象可以设置传递时的类型,也可以设置默认值等。(在实际开发中使用对象的形式较多)
除了数组之外,我们也可以使用对象,当需要对props进行类型等验证时,就需要对象写法了。String
、Number
、Boolean
、Array
、Object
、Dat
、Function
、Symbol
。验证也支持自定义的类型。
<div id="app">
<c-cpn :ccolor="color" :chmessage="message"></c-cpn>
</div>
<template id="cpn">
<div>
<h2>{{cmessage}}</h2>
<ul>
<li v-for="item in ccolor">{{item}}</li>
</ul>
<h2>{{chmessage}}</h2>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const ccpn = {
template: '#cpn',
data() {
return {
cmessage: "fo"
}
},
// props: ["ccolor", "chmessage"],
props: {
chmessage: {
type: String,
default: "hello",
required: true,
},
ccolor: {
type: Array,
default() {
return [];
}
}
}
};
const app = new Vue({
el: "#app",
data: {
message: "hello vue",
color: ["red", "black", "yellow"]
},
methods: {},
components: {
"c-cpn": ccpn,
}
}
)
</script>
子级向父级传递
当子组件需要向父组件传递数据时,就要用到自定义事件了。之前学习的v-on
不仅仅可以用于监听DOM事件,也可以用于组件间的自定义事件。
自定义事件的流程:
在子组件中,通过$emit()
来触发事件。在父组件中,通过v-on
来监听子组件事件。
在当页面应用,子组件向父组件通信中所传递的事件名称不能是驼峰式。若要修改子组件中通过父子件传来的值,不应该在子组件中直接操作,而应通过父组件。
父子组件的访问
有时候我们需要父组件直接访问子组件,子组件直接访问父组件,或者是子组件访问跟组件。
父组件访问子组件:使用$children
或$refs
。子组件访问父组件:使用$parent
。
$children
先来看下$children
的访问。this.$children
是一个数组类型,它包含所有子组件对象。
我们这里通过一个遍历,取出所有子组件的message状态。$children
的缺陷:
通过$children
访问子组件时,是一个数组类型,访问其中的子组件必须通过索引值。但是当子组件过多,我们需要拿到其中一个时,往往不能确定它的索引值,甚至还可能会发生变化。有时候,我们想明确获取其中一个特定的组件,这个时候就可以使用$refs
。
$refs
$refs的使用:$refs
和ref
指令通常是一起使用的。首先,我们通过ref
给某一个子组件绑定一个特定的ID
。其次,通过this.$refs.ID
就可以访问到该组件了。
<div id="app">
<cpn></cpn>
<cpn ref="a2"></cpn>
<cpn></cpn>
<button @click="getChildInfo">获取子组件信息</button>
</div>
<template id="cpn">
<div>
我是子组件
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const cpn = {
template: "#cpn",
data() {
return {}
}
};
const app = new Vue({
el: "#app",
data: {},
methods: {
getChildInfo() {
console.log(this.$refs.a2);
}
},
components: {
cpn,
}
})
</script>
$parent
如果想在子组件中直接访问父组件,可以通过$parent。
注意事项:
尽管在Vue开发中,我们允许通过$parent
来访问父组件,但是在真实开发中尽量不要这样做。
子组件应该尽量避免直接访问父组件的数据,因为这样耦合度太高了。如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性,往往会引起问题。另外,更不好做的是通过$parent
直接修改父组件的状态,那么父组件中的状态将变得飘忽不定,很不利于我的调试和维护。
此外,还可以通过$root
访问根组件。
非父子组件通信
使用Vuex
监听属性watch
可以通过 watch 来响应数据的变化。
里面的函数名必须为data中定义的属性,里面可以传两个值:newValue,oldValue。
<div id="app">
<cpn @num1-change="num1Change" @num2-change="num2Change" :pnum1="num1" :pnum2="num2"></cpn>
</div>
<template id="cpn">
<div>
<p>
父数据1的值:{{pnum1}}
<input type="text" v-model="cnum1">
子组件2的值:{{cnum1}}
</p>
<p>
父数据2的值:{{pnum2}}
<input type="text" v-model="cnum2">
子组件2的值:{{cnum2}}
</p>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
//子组件
const cpn = {
template: "#cpn",
data() {
return {
cnum1: this.pnum1,
cnum2: this.pnum2,
}
},
props: {
pnum1: Number,
pnum2: Number
},
methods: {},
watch: {
cnum1(newValue) {
this.cnum2 = newValue * 2;
this.$emit("num1-change", newValue);
},
cnum2(newValue) {
this.cnum1 = newValue / 2;
this.$emit("num2-change", newValue);
}
}
};
//根组件
const app = new Vue({
el: "#app",
data: {
num1: 2,
num2: 4,
},
methods: {
num1Change(value) {
this.num1 = value;
},
num2Change(value) {
this.num2 = value;
}
},
components: {
cpn,
}
})
</script>
watch 默认是浅监听。(只监听表层的变化。)对于引用类型(对象数组)的浅监听:只能监听自身一层,他的子层及以下改变监听不到。watch 如何是深度监听?在属性中设置:deep: true
。
注意:watch 监听引用类型时,是拿不到oldVal。因为指针相同,新值旧值指向同一个堆的地址。此时已经指向了新的val。
data() {
return {
msg: "这是首页",
counter: 0,
info:{
city:"beijing"
}
}
},
watch: {
//值类型,可正常拿到oldVal 和 newVal
counter: function (newVal, oldVal) {
alert('计数器值的变化 :' + oldVal + ' 变为 ' + newVal + '!');
},
//引用类型,拿不到oldVal,因为指针相同,指向同一个堆的地址。此时已经指向了新的val
info: {
handler(oval, nval) {
console.log("watch info", oval, nval);
},
deep: true //深度监听
}
}
插槽slot
使用插槽的原因
组件的插槽:
组件的插槽也是为了让我们封装的组件更加具有扩展性。让使用者可以决定组件内部的一些内容到底展示什么。
例子:移动网站中的导航栏。移动开发中,几乎每个页面都有导航栏。导航栏我们必然会封装成一个插件,比如nav-bar组件。一旦有了这个组件,我们就可以在多个页面中复用了。但是,每个页面的导航并不是一样的?
如何封装这类组件
抽取共性,保留不同。
最好的封装方式就是将共性抽取到组件中,将不同暴露为插槽。一旦我们预留了插槽,就可以让使用者根据自己的需求,决定插槽中插入什么内容。是搜索框,还是文字,还是菜单。由调用者自己来决定。
slot基本使用
在子组件中,使用特殊的元素<slot>就可以为子组件开启一个插槽。该插槽插入什么内容取决于父组件如何使用。
1.插槽的基本使用 <slot></slot>
2.插槽的默认值 <slot>button</slot>
3.如果有多个值, 同时放入到组件进行替换时, 一起作为替换元素。
具名插槽slot
当子组件的功能复杂时,子组件的插槽可能并非是一个。比如我们封装一个导航栏的子组件,可能就需要三个插槽,分别代表左边、中间、右边。那么,外面在给插槽插入内容时,如何区分插入的是哪一个呢?
这个时候,我们就需要给插槽起一个名字。
如何使用具名插槽呢?
非常简单,只要给slot元素一个name属性即可<slot name='myslot'></slot>
给出一个案例:这里我们先不对导航组件做非常复杂的封装,先了解具名插槽的用法。
<div id="app">
<cpn>
<h4 slot="middle">haha</h4>
</cpn>
</div>
<template id="cpn">
<div>
<slot>左边</slot>
<slot name="middle">中间</slot>
<slot>右边</slot>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {},
methods: {},
components: {
cpn: {
template: "#cpn",
}
}
})
</script>
//左边
//haha
//右边
作用域插槽
编译作用域
先看一个例子:考虑下面的代码是否最终是可以渲染出来的:
答案:最终可以渲染出来,也就是使用的是Vue实例的属性。
解释:官方给出了一条准则:父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。而在使用<my-cpn v-show="isShow"></my-cpn>
的时候,整个组件的使用过程是相当于在父组件中出现的。那么他的作用域就是父组件,使用的属性也是属于父组件的属性。因此,isShow使用的是Vue实例中的属性,而不是子组件的属性。
作用域插槽
父组件对子组件展示数据的方式不满意,他要以自己方式展示,就需要从子组件中获取数据。父组件替换插槽的标签,但是内容由子组件来提供。
说明:
子组件中:data
也可以其他名称,但是不能使用驼峰式。
在vue2.5.x以上的版本中,<template>
可以替换为其他标签
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。