组件的定义:实现应用中局部
功能代码
和资源
的集合。
(一) 组件化基础
[1]. 全局组件和局部组件
注意点:
- 关于组件名:
一个单词组成:
第一种写法(首字母小写):school
第二种写法(首字母大写):School
多个单词组成:
第一种写法(kebab-case命名):my-school
第二种写法(CameCase命名):MySchool(需要脚手架支持)- 关于组件标签:
第一种写法:<school><school/>
第二种写法:<school /> (需要使用脚手架,否则导致后续组件不能渲染)- 一个简写
const school = Vue.extend(options) 可简写为:const school = options;- 组件中的this
每次调用Vue.extend,返回的都是一个全新的VueComponent。
组件配置中,data函数、methods中的函数、watch中的函数、computed中的函数,他们中使用的this均是【VueComponent实例对象】
-
全局组件
全局组件可以在任意Vue实例下使用Vue.component('my-component-name', { // ... 选项 ... })
-
局部组件
如果我们注册的组件是挂载在某个实例中,那么就是一个局部组件new Vue({ el: '#app', components: { 'component-a': ComponentA, 'component-b': ComponentB } })
-
用例
<div id="app"> <!-- 3.使用全局组件 --> <my-com></my-com> <!-- 3.使用局部组件 --> <my-com2></my-com2> </div> <div id="app2"> <!-- 3.使用全局组件 --> <my-com></my-com> </div> <script src="./vue.js"></script> <script> //1.创建组件构造器对象 const cpnc = Vue.extend({ template: '<div><h2>我是全局组件</h2></div>' }); //2.注册全局组件,并且定义组件标签的名称 Vue.component('my-com', cpnc); //3.注册局部组件,并将vue-devTools中显示该组件的名称改为 part const MyCom2 = { //省略Vue.extend() name: 'part', template: ` <div> <p>{{info}}</p> </div>`, data() { return { info: '我是局部组件' } } } const vm = new Vue({ el: "#app", components: { //组件名 : 组件模板 'my-com2': MyCom2 } }); const app2 = new Vue({ el: "#app2", }); </script>
[2]. 父组件和子组件
-
VueComponent.prototype.__proto__ = Vue.prototype
<div id="app"> <parent></parent> </div> <script src="./vue.js"></script> <script> //1.创建子组件 const son = Vue.extend({ template: '<div><h4>{{info}}</h4></div>', data() { return { info: '我是子组件' } } }); //2.创建父组件 const parent = Vue.extend({ template: ` <div> <h2>{{info}}</h2> <!-- 4.在父组件中使用子组件 --> <son></son> </div>`, //3.在父组件中注册子组件 components: { son: son }, data() { return { info: '我是父组件' } } }); //root 组件 const vm = new Vue({ el: "#app", components: { //在根组件中注册父组件 parent //parent: parent的简写 } }); </script>
[3]. 注册组件的语法糖
主要是省去了调用Vue.extend()的步骤,可以直接使用一个对象来代替。
<div id="app">
<my_pnc1></my_pnc1>
<my_pnc2></my_pnc2>
</div>
<script src="./vue.js"></script>
<script>
//1.创建组件构造器对象(语法糖)
var my_pnc1 = { //省略了Vue.extend()
template: `
<div>
<h2>{{info}}</h2>
</div>`,
data() {
return {
info: '我是全局组件'
}
}
}
Vue.component('my_pnc1', my_pnc1); //创建全局组件
//2.创建组件构造器对象(语法糖)
let my_pnc2 = { //省略了Vue.extend()
template: `
<div>
<h2>{{info}}</h2>
</div>`,
data() {
return {
info: '我是局部组件'
}
}
}
const app = new Vue({
el: "#app",
components: {
my_pnc2: my_pnc2 //这里会自动判断是否省略了Vue.extend(),如果省略,则会自动调用
}
});
</script>
[4]. 抽离组件的模板
-
方式一:使用<script>标签,注意类型为text/x-template
-
方式二:使用<template>标签
<div id="app"> <my_pnc1></my_pnc1> <my_pnc2></my_pnc2> </div> <!-- 1.使用script标签 --> <script type="text/x-template" id="my_pnc1"> <div> <h2>我是标题2</h2> <p>我是副标题2</p> </div> </script> <!-- 2.使用template标签 --> <template id="my_pnc2"> <div> <h2>我是标题2</h2> <p>我是副标题2</p> </div> </template> <script src="./js/vue.js"></script> <script> Vue.component('my_pnc1', { template:'#my_pnc1', }); Vue.component('my_pnc2',{ template: '#my_pnc2' }); const app = new Vue({ el:"#app", }); </script>
[5]. 组件数据存放
-
组件是一个单独功能模块的封装:
这个模块有属于自己的HTML模板,也有属于自己的data数据。 -
组件有自己的data和methods等属性,data属性是一个函数,返回一个对象,对象内部保存着数据。
-
组件中的data设计成函数,避免多次使用组件时相互影响,当每次调用组件时,会返回一个全新的对象。如果data是一个对象,当多次调用组件时会造成数据黏连(第一个组件改变了data中的数据,第二个组件的数据也会跟着改变),在7中的例子可以体现出来
<div id="app"> <h2>{{info}}</h2> <cpn></cpn> <cpn></cpn> </div> <template id="my_pnc1"> <div> <p>当前的数字为:{{num}}</p> <button @click="add">+</button> <button @click="sub">-</button> </div> </template> <script src="./vue.js"></script> <script> const cpn = { template: '#my_pnc1', data() { //组件中的data是一个方法 return { num: 0 } }, methods: { add() { this.num++; }, sub() { this.num--; } }, } const vm = new Vue({ el: "#app", data: { //vue实例的data是一个对象 info: '各个组件间的data相互独立' }, components: { cpn //注册组件 } }); </script>
[6]. 使用ref获取元素或子组件的引用信息
- ref应用在html标签上获取的是真实DOM元素(id的替代者),应用在组件标签上获取的是组件实例对象(VueComponent)
- 使用方式:(School是组件)
打标识: <h1 ref=“xxx”> …</h1> 或 <School ref=“xxx”></School>
获取:this.$refs.xxx<div id="app"> <p ref="demo">案例</p> <School ref="comSchool"></School> <button @click="getRef()">点击获取ref</button> </div> <template id="school"> <div> <p>{{name}}</p> <p>{{address}}</p> </div> </template> <script src="./vue.js"></script> <script> const School = { name: 'School', template: "#school", data() { return { name: '学校名称', address: '学校地址' } } }; new Vue({ el: '#app', components: { School }, methods: { getRef() { console.log(this.$refs.demo); //<p>案例</p> console.log(this.$refs.comSchool); //组件实例Vuecomponent } }, }) </script>
[7]. 父组件访问子组件
①. $refs
- ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。
- 如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件。
- 我们通过ref给一个子组件绑定一个特定的
ID
。通过this.$refs.ID
就可以访问到该组件了。<div id="app"> <cpn></cpn> <cpn ref="id02"></cpn> <button @click="getCpn">获取子组件</button> </div> <template id="cpn"> <h2>我是子组件</h2> </template> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> const vm = new Vue({ el:"#app", //创建子组件 components:{ cpn:{ template: '#cpn', data() { return { name: '我是子组件的name' } }, methods:{ getSonCpn(){ console.log('我是子组件的方法'); } } } }, methods:{ getCpn(){ //以对象的形式存储子组件 console.log(this.$refs); console.log(this.$refs.id02.name);//获取到子组件的data中的name信息 this.$refs.id02.getSonCpn();//获取到子组件的methods中的getSonCpn方法 } } }); </script>
②. $children
-
$children 以数组的形式存储所有子组件
-
$children访问子组件时,是一个数组类型,访问其中的子组件必须通过索引值。当子组件过多时,如果需要拿到其中某一个时,如果此时代码中写的固定的索引值,在以后添加另一个子组件时是会出错的。
<div id="app"> <cpn></cpn> <cpn></cpn> <button @click="getCpn">获取子组件</button> </div> <template id="cpn"> <h2>我是子组件</h2> </template> <script src="./js/vue.js"></script> <script> const app = new Vue({ el:"#app", //创建子组件 components:{ cpn:{ template: '#cpn', data() { return { name: '我是子组件的name' } }, methods:{ getSonCpn(){ console.log('我是子组件的方法'); } } } }, methods:{ getCpn(){ //以数组的形式存储所有子组件 console.log(this.$children); console.log(this.$children[0].name); this.$children[0].getSonCpn(); } } }); </script>
[8]. 父子组件的通信
①. 父组件通过props(properties)向子组件传递数据
-
1). 在子组件中,需要使用props选项来声明从父级接收到的数据。
-
2). props接收值的两种方式:
- 方式一:字符串数组 (数组中的字符串就是传递时的名称)。
- 方式二:对象 (对象可以设置传递时的类型,也可以设置默认值等)。
- [1] 当需要对props进行类型等验证时,就需要对象写法。
- [2] 验证支持的类型有:String,Number,Boolean,Array,Object,Date,Function,Symbol,自定义验证的类型。
- [3] 子组件中的props属性中不支持驼峰法命名,因为在“将父组件中的数据传递给子组件”时,v-bind不支持驼峰命名(可以将v-bind:cInfo变为v-bind:c-info)。
- [4] 子组件模板中包含多个标签时,必须都被同一个根标签包着。
- [5] props是只读的,如果需要修改,则需要赋值props的内容到data中,然后修改data中相应的数据。
- [6] https://cn.vuejs.org/v2/guide/components-props.html
<div id="app"> <!-- 4.将父组件中的数据传递给子组件 --> <!-- 如果直接传值(如stime),则v-bind非必须 --> <!-- 如果直接传值,传递的是数字(如sfun)则需要添加v-bind,否则传递过去的是一个字符串'1' --> <cpn v-bind:smovies="movies" stime="2020-03-02" :ssum="sum" :svalid="1" :sfn="myFn"></cpn> </div> <template id="soncpn"> <div> <p>sNull:{{sNull}}</p> <p>smovies:{{smovies}}</p> <p>myTime:{{myTime}}</p> <p>ssum:{{ssum}}</p> <p>smessage:{{smessage}}</p> <p>svalid:{{svalid}}</p> <button @click="sfn">sfn</button> </div> </template> <script src="./vue.js"></script> <script> //1.创建子组件 const cpn = { template: '#soncpn', data() { return { myTime: this.stime //data中获取props中的数据 } }, //2.创建变量,将父组件中的movies数据保存到子组件的smovies中 //2.1 通过字符串数组接收数据 //props: ['smovies', 'time'] //2.2 通过对象接收数组 props: { //可以匹配任何类型,这里不传参 sNull: null, //父组件传递过来的数据必须是字符串类型 stime: String, //可以是多个类型 ssum: [String, Number], //必须传参,而且是数组类型 smovies: { type: Array, required: true //表示必须传参 }, //带有默认值的对象 smessage: { type: Object, //对象或者数组默认值必须从一个工厂函数获取 default: function () { return { message: 'hello' } } }, //自定义验证函数 svalid: { validator: function (value) { //value值必须为1,当传递的参数不为1时报错 return value === 1; } }, sfn: { type: Function } } }; new Vue({ el: "#app", //用于挂载要管理的元素 data: { //定义数据 movies: ['电影1', '电影2', '电影3'], sum: 100, }, components: { //3.将子组件在父组件中注册 //下边的相当于 cpn:cpn (即cpn:{...}) 是对象字面量增强写法中的属性增强写法 cpn }, methods:{ myFn(){ //传递一个方法 console.log('一个方法'); } } }); </script>
② 子组件通过props和闭包向父组件传递数据
原理:父组件通过向子组件发送一个闭包来实现通信。
<div id="app">
{{message}}
<cpm :parent-send-info="getSonData"></cpm>
</div>
<template id="cpm">
<div>
<p>{{info}}</p>
<button @click="sendData">子组件发送数据</button>
</div>
</template>
<script src="./vue.js"></script>
<script>
let cpm = {
template: '#cpm',
data() {
return {
info: '我是子组件',
msg: '子组件向父组件传递的信息'
}
},
props: {
'parentSendInfo': {
type: Function,
required: true
}
},
methods: {
sendData() {
this.parentSendInfo(this.msg)
}
}
}
const vm = new Vue({
el: "#app",
data: {
message: ''
},
components: {
cpm
},
methods: {
getSonData(msg) {
this.message = msg;
console.log(this);//vue实例(vm)
}
},
});
</script>
③. 子组件通过自定义事件向父组件传递数据($emit)
-
在子组件中通过$emit(‘事件名称’ [, 参数] ) 触发事件。($emit中的事件名称不能使用驼峰法)
-
父组件监听子组件中通过$emit发射过来的btn-click事件,事件的回调函数在父组件中。
-
自定义事件可以使用事件的修饰符 (如:.once)
-
组件上也可以绑定原生DOM事件(如:click),但是需要事件修饰符.native
<!-- 1.父组件 --> <div id="app"> <!-- 4.父组件监听子组件中通过$emit发射过来的btn-click事件 --> <!-- btn-click被绑定到 子组件(cpn)的VueComponent的实例对象上 --> <cpn v-on:btn-click="parentReceive"></cpn> </div> <!-- 2.子组件 --> <template id="cpn"> <div> <!-- 3.点击子组件按钮将当前子组件信息提交给父组件 --> <button v-for="item in btn" @click="btnClick(item)">{{item.name}}</button> </div> </template> <script src="./js/vue.js"></script> <script> const cpn = { template: "#cpn", data(){ return { btn :[ {id:1,name:'按钮1'}, {id:2,name:'按钮2'}, {id:3,name:'按钮3'}, ] } }, methods: { btnClick(item){ //通过$emit('事件名称', '参数') this.$emit('btn-click', item); } } }; const app = new Vue({ el :"#app", data:{ message:"hello world!" }, components: { cpn }, methods: { //父组件监听子组件的方法 parentReceive(son){ console.log('父组件接收', son); } } }); </script>
④ 子组件通过自定义事件向父组件传递数据($on)
<!-- 1.父组件 -->
<div id="app">
{{info}}
<!-- 4.通过ref获取子组件实例VueComponent -->
<cpn ref="demo"></cpn>
</div>
<!-- 2.子组件 -->
<template id="cpn">
<div>
<!-- 3.点击子组件按钮将当前子组件信息提交给父组件 -->
<button @click="sendInfo">点击发送信息</button>
</div>
</template>
<script src="./vue.js"></script>
<script>
const cpn = {
template: "#cpn",
data() {
return {
info: '我是子组件中的信息'
}
},
methods: {
sendInfo() {
//子组件触发父组件绑定给子组件的listen事件
this.$emit('listen', this.info);
}
},
};
const vm = new Vue({
el: "#app",
components: {
cpn
},
data: {
info: ''
},
mounted() {
//页面挂载完成后,给子组件的实例对象VueComponent绑上listen事件
//$on的回调函数如果是非箭头函数,则this是子组件实例VueComponent
//这种方式可以控制在什么时间给子组件绑定事件
this.$refs.demo.$on('listen', (msg) => {
console.log(this);
this.info = msg;
})
},
});
</script>
⑤ 祖组件向孙组件传递数据: provide / inject
- provide 和 inject 绑定并不是可响应的。如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。
- 可以使用 对象、函数等方法使孙组件获得响应式的数据
用例1:
<!-- 1.根组件 -->
<div id="app">
{{info}}
{{obj.message}}
<button @click="editInfo">修改根组件的info</button>
<button @click="editObj">修改obj的message</button>
<cpn></cpn>
</div>
<!-- 2.子组件 -->
<template id="cpn">
<div>
{{info}}
<grandson></grandson>
</div>
</template>
<!-- 3.孙组件 -->
<template id="grandson">
<div>
{{info}}
<!-- 方式一 、 二 的显示 -->
<!-- <span style="color: red;">{{message}}</span> -->
<!-- 方式三的显示 -->
<!-- <span style="color: red;">{{obj.message}}</span> -->
<!-- 方式四的显示, 可以将msgFn() 放入到计算属性中,更方便的使用 -->
<span style="color: red;">{{msgFn()}}</span>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
<script>
const grandson = {
template: "#grandson",
// inject: ['message'], // 方式一、二的接收
// inject: ['obj'], // 方式三接收
inject: ['msgFn'], // 方式四接收
data() {
return {
info: '我是孙组件中的信息'
}
}
};
const cpn = {
template: "#cpn",
data() {
return {
info: '我是子组件中的信息'
}
},
components: {
grandson
}
};
const vm = new Vue({
el: "#app",
components: {
cpn
},
//方式一: 给孙组件传递一个简单信息
// provide: {
// message: '我是根组件传的信息'
// },
//方式二:给孙组件传递一个data 中的信息 (孙组件获取的是非响应式的数据)
// provide() {
// return {
// message: this.info
// }
// },
// 方式三:如果希望孙组件获得的是一个响应式的数据,将数据放在一个 对象中
// provide() {
// return {
// obj: this.obj
// }
// },
provide() {
return {
msgFn: () => this.info // 方式四:如果希望孙组件获得的是一个响应式的数据,将数据放在一个 函数的返回值中
}
},
data: {
info: '我是根(祖先)组件',
obj: {
message: '我是祖先组件的obj.message'
}
},
methods: {
editInfo() {
this.info = '新的info信息'
},
editObj() {
this.obj.message = '新的一条响应式的消息'
}
},
});
</script>
用例2:
<!-- 祖先组件 -->
<div id="app">
<div style="border: 1px solid green">
我是祖先组件
<p>sum: {{ sum }}</p>
<p>movies:{{movies}}</p>
<p>moviesLength:{{moviesLength}}</p>
<p>obj:{{obj}}</p>
<button @click="updateAge">修改obj.age</button>
<button @click="sum++">修改sum</button>
<button @click="addMovies">添加movies</button>
<Child />
</div>
</div>
<!-- 中间组件(可以多层) -->
<template id="child">
<div style="border:1px solid red">
我是中间组件
<Descendant />
</div>
</template>
<!-- 后代组件 -->
<template id="descendant">
<div style="border: 1px solid blue;">
我是后代组件
<p>otherMovies:{{otherMovies}}</p>
<p>getAncestorSum:{{getAncestorSum}}</p>
<p>obj:{{obj}}</p>
<p>msg:{{msg}}</p>
<p>moviesLength: {{moviesLength}}</p>
<p>sum:{{sum}}</p>
<p>otherSum:{{otherSum()}}</p>
<button @click="getAncestorSum">调用getAncestorSum</button>
<button @click="updateName">修改obj.name</button>
<button @click="updateMsg">修改msg</button>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
<script>
// 后代组件,获取祖先组件传递的数据、方法等
const Descendant = {
template: '#descendant',
inject: ['otherMovies', 'getAncestorSum', 'obj', 'msg', 'moviesLength', 'sum', 'otherSum'],
methods: {
updateMsg() {
this.msg = '新消息' //报错,提示不要修改msg
},
updateName() {
this.obj.name = '王五'//不报错,响应式
}
}
};
// 中间组件充当过渡
const Child = {
template: '#child',
components: {
Descendant
}
};
new Vue({
el: "#app",
components: {
Child
},
provide() {
return { //要传递给后代组件的数据、方法
otherMovies: this.movies, // movies (key) 没有必须与值一样
getAncestorSum: this.getAncestorSum, //sum将被响应式的修改
obj: this.obj,
msg: '不要捣蛋',
moviesLength: this.moviesLength, // 计算属性非响应式
sum: this.sum, // sum 为非响应式
otherSum: () => this.sum // sum为响应式
}
},
data: {
movies: ['电影1', '电影2', '电影3'],
sum: 100,
obj: {
name: '张三',
age: 24
}
},
computed: {
moviesLength() {
return this.movies.length
}
},
methods: {
getAncestorSum() {
console.log('祖先组件中的getAncestorSum方法被触发');
this.sum++
},
updateAge() {
this.obj.age = 28
},
addMovies() {
this.movies.push('电影' + (this.movies.length + 1))
}
}
});
</script>
[9]. 子组件访问父组件和根组件:$parent,$root
-
尽管在Vue开发中,我们允许通过$parent来访问父组件,但是在真实开发中尽量不要这样做。
-
子组件应该尽量避免直接访问父组件的数据,因为这样耦合度太高了。
-
如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性,往往会引起问题。
-
另外,更不好做的是通过$parent直接修改父组件的状态,那么父组件中的状态将变得飘忽不定,很不利于我的调试和维护
<div id="app"> <cpn></cpn> </div> <template id="cpn"> <div> <h2>我是子组件</h2> <button @click="getParent">获取父组件1</button> <ccpn></ccpn> </div> </template> <template id="ccpn"> <div> <h2>我是孙组件</h2> <button @click="getParent2">获取父组件2</button> </div> </template> <script src="./js/vue.js"></script> <script> const app = new Vue({ el:"#app", //创建子组件 components:{ cpn:{ template: '#cpn', data() { return { name: '我是子组件的name' } }, methods:{ getParent(){ //获取的是vue实例 console.log(this.$parent); console.log(this.$parent.name); this.$parent.getParentFun() }, getParentFun(){ console.log('我是子组件的方法'); } }, //创建一个孙组件 components:{ ccpn: { template:"#ccpn", methods:{ getParent2(){ //获取的是VueComponent实例 console.log(this.$parent); console.log(this.$parent.name); this.$parent.getParentFun(); //获取跟组件,即vue实例 console.log(this.$root); console.log(this.$root.name); } } } } } }, methods:{ getParentFun(){ console.log('我是父组件的方法'); } }, data:{ name:'我是父组件' } }); </script>
[10]. 兄弟组件间的通信
①. 方式一:通过父组件进行兄弟组件之间通讯
让兄弟组件通过一个共同的父组件彼此通讯。
子组件1通过$emit将信息交给父组件,父组件通过props将信息传递给子组件2。
<div id="card" style="border:1px solid green;">
<div class="card-header">
<h5 v-text="theCardTitle"></h5>
</div>
<div class="card-body">
<brother-card :message-son="messageSon" @brother="messageDaughterFun"></brother-card>
<sister-card :message-daughter="messageDaughter" @sister="messageSonFun"></sister-card>
</div>
</div>
<template id="SisterCard">
<div class="message" style="border:1px solid blue; margin:2px;">
<div class="message-header">
<h5 v-text="theCardTitle"></h5>
</div>
<div class="message-body">
<p class="message-text">我是Sister组件</p>
<button @click="messageBrother" class="btn">给哥哥发消息</button>
<div v-if="messageDaughter" class="alert" v-html="messageDaughter"></div>
</div>
</div>
</template>
<template id="BrotherCard">
<div class="message" style="border:1px solid red; margin:2px;">
<div class="message-header">
<h5 v-text="theCardTitle"></h5>
</div>
<div class="message-body">
<p class="message-text">我是Brother组件</p>
<button @click="messageSister" class="btn">给妹妹发消息</button>
<div v-if="messageSon" class="alert" v-html="messageSon"></div>
</div>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const SisterCard = {
template: "#SisterCard",
props: ['messageDaughter'],
data: () => ({
theCardTitle: '子组件2'
}),
methods: {
messageBrother() {
this.$emit('sister', '妹妹发送给哥哥的消息')
}
}
};
const BrotherCard = {
template: "#BrotherCard",
props: ['messageSon'],
data: () => ({
theCardTitle: '子组件1'
}),
methods: {
messageSister() {
this.$emit('brother', '哥哥发送给妹妹的消息')
}
}
};
const app = new Vue({
el: "#card",
data: () => ({
theCardTitle: '父组件',
messageDaughter: '',
messageSon: ''
}),
components: {
BrotherCard,
SisterCard
},
methods: {
messageDaughterFun(message) {
this.messageDaughter = message;
},
messageSonFun(message) {
this.messageSon = message;
}
}
});
</script>
②. 方式二:通过事件总线进行兄弟间组件通讯
-
一种组件间通信的方式,适用于
任意组件间通信
。 -
总线就是在Vue的prototype上添加一个Vue / VueComponent实例。
-
可以使用入口Vue自身当做总线(推荐、标准)
new Vue({ el: '#app', beforeCreate() { //安装全局事件总线,将当前的Vue实例(this)给Vue原型对象$bus,充当中转站 Vue.prototype.$bus = this }, })
-
可以使用一个Vue实例当做总线
Vue.prototype.$bus = new Vue();//给$bus赋值一个Vue实例
-
可以使用组件VueComponent实例当作总线
let Dome = Vue.extend({}); Vue.prototype.$bus = new Dome();//给$bus赋值一个组件实例(VueComponent实例)
<div id="app">
<cpn></cpn>
<hr />
<cpn2></cpn2>
</div>
<template id="cpn">
<div>
<p>{{info}}</p>
<p style="color: red;">{{getCpn2Msg}}</p>
<button @click="senCpn2Msg">向组件2发送信息</button>
</div>
</template>
<template id="cpn2">
<div>
<p>{{info}}</p>
<p style="color: green;">{{getCpnMsg}}</p>
<button @click="senCpnMsg">向组件1发送信息</button>
</div>
</template>
<script src="./vue.js"></script>
<script>
var cpn = {
template: '#cpn',
data() {
return {
info: '我是组件1',
getCpn2Msg: '' //获取组件2给的消息
}
},
methods: {
senCpn2Msg() {
//因为VueComponent.prototype.__proto__ = Vue.prototype
//所以组件可以找到绑定在Vue上$bus
this.$bus.$emit('listenCpnSend', '组件1给组件2发送的信息')
}
},
mounted() {
//要使用箭头函数 确保this的指向
this.$bus.$on('listenCpn2Send', (msg) => {
this.getCpn2Msg = msg;
});
},
beforeDestroy() {
// 销毁当前组件时,要解绑该组件绑定在总线上的自定义事件
//注意:如果$off()没有参数则会解绑总线$bus上的所有自定义事件
this.$bus.$off('listenCpn2Send');
},
};
var cpn2 = {
template: '#cpn2',
data() {
return {
info: '我是组件2',
getCpnMsg: '' //获取组件1给的消息
}
},
methods: {
senCpnMsg() {
this.$bus.$emit('listenCpn2Send', '组件2给组件1发送的信息')
},
getCpnInfo(msg){
this.getCpnMsg = msg;
}
},
mounted() {
this.$bus.$on('listenCpnSend', this.getCpnInfo);
},
beforeDestroy() {
this.$bus.$off('listenCpnSend');
},
};
new Vue({
el: '#app',
components: {
cpn,
cpn2
},
beforeCreate() {
//安装全局事件总线,将当前的Vue实例(this)给Vue原型对象$bus,充当中转站
Vue.prototype.$bus = this
},
})
</script>
[11]. 组件的 $el获取组件内的html元素
每个组件都有$el属性,用于获取该组件内的html元素,在mounted生命周期中才有效。
<div id="app">
<button @click="getEl">获取el</button>
<button @click="getChildrenEl">获取cpn的el</button>
<cpn ref="cpn"></cpn>
</div>
<template id="cpn">
<p>cpn</p>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const cpn = {
template:"#cpn",
}
new Vue({
el: "#app",
components: {
cpn
},
methods: {
getEl(){
console.log(this.$el);//<div id="app">...</div>
},
getChildrenEl(){
console.log(this.$refs.cpn.$el); //<p>cpn</p>
}
}
});
</script>
[14]. 解绑自定义事件$off
-
使用$off([参数]),可以解绑自定义的事件,参数为自定义事件名字符串,则表示解绑一个自定义事件,如果为自定义事件名数组,则表示解绑多个自定义事件,如果没有参数,则表示解绑所有自定义事件。
-
组件被销毁(destroy钩子函数被触发)则该组件自动解绑所有自定义事件。该组件的所有子组件的自定义事件也自动解绑。(组件销毁后,组件和子组件的原生事件(如click)是不会解绑的)
<div id="app"> <button @click="killSelf">销毁父组件</button> <cpn @custom1="listenCpn" @custom2="listenCpn" @custom2="listenCpn"></cpn> </div> <template id="cpn"> <div> <button @click="clickFn1">自定义事件1</button> <button @click="clickFn2">自定义事件2</button> <button @click="clickFn3">自定义事件3</button><br /> <button @click="offOne">解绑事件1</button> <button @click="offTwo">解绑事件2和事件3</button> <button @click="killThis">销毁子组件</button> </div> </template> <script src="./vue.js"></script> <script> var cpn = { template: '#cpn', methods: { clickFn1() { //只解绑自定义事件,该原生方法(click)还会触发 console.log('原生click事件不会失效'); this.$emit('custom1', 1) }, clickFn2() { this.$emit('custom2', 2) }, clickFn3() { this.$emit('custom3', 3) }, offOne() { this.$off('custom1') }, offTwo() { this.$off(['custom2', 'custom3']) }, killThis() { //所有自定义事件失效 this.$destroy(); } }, } new Vue({ el: '#app', components: { cpn }, methods: { listenCpn(id) { console.log('自定义事件' + id); }, killSelf() { this.$destroy(); //子组件自定义事件失效 }, }, }) </script>
[15]. 动态组件 component
https://cn.vuejs.org/v2/guide/components.html#%E5%8A%A8%E6%80%81%E7%BB%84%E4%BB%B6
当多个组件组成一个大页面时,如果组件类型/顺序不确定时,可以使用动态组件。
<div id="app">
<ul>
<li v-for="item in cpns">
<component v-bind:is="item"></component>
</li>
</ul>
</div>
<template id="cpn">
<p>组件1</p>
</template>
<template id="cpn2">
<p>组件2</p>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const cpn = {
template: "#cpn"
}
const cpn2 = {
template: "#cpn2"
}
const app = new Vue({
el: "#app",
data: {
cpns: [
'cpn',
'cpn2',
'cpn'
]
},
components: {
cpn,
cpn2,
}
});
</script>
[16]. 缓存组件 keep-alive
① 用例
-
keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染(如果没有keep-alive组件是会被销毁和重新创建,添加keep-alive是不会被销毁的)。
-
keep-alive实例:
<div id="app"> <button @click="currentCpn = 'dateCpn'">btn1</button> <button @click="currentCpn = 'otherDateCpn'">btn2</button> <!-- 可以看看去掉keep-alive的效果 --> <keep-alive> <component :is="currentCpn"></component> </keep-alive> </div> <template id="cpn"> <p>dateTime: {{dateTime}}</p> </template> <template id="cpn2"> <p>otherCpn: {{dateTime}}</p> </template> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> const dateCpn = { template: "#cpn", data() { return { dateTime: new Date() //可以实现响应式改变 } }, mounted() { console.log('cpn mounted'); }, destroyed() { console.log('cpn destroyed'); } } const otherDateCpn = { template: "#cpn2", data() { return { dateTime: new Date() //可以实现响应式改变 } }, mounted() { console.log('cpn2 mounted'); }, destroyed() { console.log('cpn2 destroyed'); } } const app = new Vue({ el: "#app", data: { currentCpn: 'dateCpn' }, components: { dateCpn, otherDateCpn } }); </script>
②. include 、 exclude 和 max
匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值)。匿名组件不能被匹配。
- include - 字符串或正则表达,只有匹配的组件会被缓存
<!-- Detail是组件的name属性的值 --> <!-- 缓存多个 可以使用 :include="['dateCpn', 'otherDateCpn']" --> <keep-alive include="dateCpn"> <component :is="currentCpn"></component> </keep-alive>
- exclude - 字符串或正则表达式,任何匹配的组件都不会被缓存
<!-- 不缓存dateCpn(组件的name的值) --> <keep-alive exclude="dateCpn"> <component :is="currentCpn"></component> </keep-alive>
- max - 最多可以缓存多少组件实例。一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉。
③. activated 和 deactivated 生命周期钩子
- 对于缓存的组件来说,再次进入时,我们是不会执行
created
或mounted
等生命周期 - 当组件在<keep-alive> 内被切换,它的
activated
和deactivated
这两个生命周期钩子函数将会被对应执行。 - 在 2.2.0 及其更高版本中,
activated
和deactivated
将会在 <keep-alive> 树内的所有嵌套组件中触发。
[17]. 异步组件(异步加载组件)
https://cn.vuejs.org/v2/guide/components-dynamic-async.html#%E5%BC%82%E6%AD%A5%E7%BB%84%E4%BB%B6
当使用局部注册的时候,你也可以直接提供一个返回 Promise 的函数:
new Vue({
// ...
components: {
'my-component': () => import('./my-async-component')
}
})
[18]. 组件构造函数VueComponent
- 组件(如上边的my-component)本质是一个名为VueComponent的构造函数,由Vue.extend生成。
- 当使用组件时(如<my-component></my-component>),Vue解析时会帮我们创建my-component组件的实例对象,即Vue帮我们执行:new VueComponent(option)。
- 特别注意:每次调用Vue.extend,返回的都是一个全新的VueComponent,生成全新的实例对象。
- 关于this指向:
(1). 组件配置中:
data函数、methods中的函数、watch中的函数、computed中的函数,他们的this均是【VueComponent的实例对象】
(2). new Vue(option)配置中:
data函数、methods中的函数、watch中的函数、computed中的函数,他们的this均是【Vue的实例对象】 - VueComponent 和 Vue的关系:VueComponent.prototype.__proto__ = Vue.prototype
(二) 组件化高级
[1].插槽 slot
-
组件的插槽是为了让我们封装的组件具有更好的扩展性
-
插槽的基础使用
<div id="app"> <!-- 2.组件中没有定义其他标签将使用slot默认值 --> <cpn></cpn> <!-- 3.组建中使用一个标签将替换slot默认值 --> <cpn> <p>我是p标签</p> </cpn> <!-- 4.组件中有多个标签将替换slot默认值 --> <cpn> <p>我是p标签</p> <i>我是i标签</i> </cpn> </div> <template id="cpn"> <div> <h2>我是子组件</h2> <!-- 1.slot可以有默认值也可以没有,这里使用默认值button --> <slot><button>sloat</button></slot> </div> </template> <script src="./vue.js"></script> <script> const app = new Vue({ el: "#app", components: { cpn: { template: '#cpn', } } }); </script>
[2].具名插槽 slot
①. vue2.6.0 - 使用具名插槽
v-slot 指令自 Vue 2.6.0 起被引入,提供更好的支持 slot 和 slot-scope attribute 的 API 替代方案。在接下来所有的 2.x 版本中 slot 和 slot-scope attribute 仍会被支持,但已经被官方废弃且不会出现在 Vue 3 中。
<div id="app">
<!-- 1.替换没有命名的slot -->
<cpn>
<button>替换没有命名的</button>
</cpn>
<!-- 2.替换中间的slot -->
<cpn>
<span slot="middle">替换中间</span>
</cpn>
</div>
<template id="cpn">
<div>
<slot name="left"><span>左边</span></slot>
<slot name="middle"><span>中间</span></slot>
<slot name="right"><span>右边</span></slot>
<slot><p>被替换</p></slot>
</div>
</template>
<script src="./js/vue.js"></script>
<script>
const app = new Vue({
el :"#app",
data:{
message:"hello world!"
},
components:{
cpn: {
template: "#cpn"
}
}
});
</script>
②. vue2.6.0 + 使用具名插槽
<div id="app">
<cpn>
<template v-slot:btn>
<button>替换btn</button>
</template>
<!-- #btn2是v-slot:btn2的缩写 -->
<template #btn2>
<p>btn2被替换掉</p>
</template>
</cpn>
</div>
<template id="cpn">
<div>
<slot name="btn">
<button>默认btn</button>
</slot>
<slot name="btn2">
<button>具名插槽的btn2</button>
</slot>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const cpn = {
template: '#cpn',
}
const app = new Vue({
el: "#app",
components: {
cpn
}
});
</script>
[3].编译作用域
- 父组件模板的所有东西会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。
<div id="app"> <!-- 这里的isShow使用的是父组件中的 --> <cpn v-show="isShow"></cpn> </div> <template id="cpn"> <div> <p>文字文字文字文字文字文字</p> <p>文字文字文字文字文字文字</p> </div> </template> <script src="./vue.js"></script> <script> const cpn = { template: '#cpn', data() { return { isShow: false } } } new Vue({ el: "#app", data: { isShow: true }, components: { cpn } }); </script>
[4].作用域插槽
②. 2.6.0 - 使用作用域插槽
-
父组件替换子组件插槽的标签,但是内容还是由子组件提供。
-
slot-scope="mySlot"对应的mySlot有一个属性 .row 通过mySlot.row可以获取到当前(行)的所有数据
-
自 2.6.0 起已废弃的使用 slot-scope,被 v-slot 代替
<div id="app"> <Cpn> <!-- 2.通过slot-scope获取到绑定在子组件上的books数据存放在mySlot变量上 --> <template slot-scope="mySlot"> <ul> <li v-for="book in mySlot.bs">{{book}}</li> </ul> </template> </Cpn> </div> <template id="cpn"> <div> <!-- 1.将子组件的books数据绑定在自定义的bs变量上 --> <slot :bs="books"> </slot> </div> </template> <script src="./vue.js"></script> <script> const Cpn = { template: "#cpn", data() { return { books: ['C语言编程', "C#一站式编程", "C指针", "PHP 基础"], } } } const vm = new Vue({ el: "#app", data: { message: "hello world!" }, components: { Cpn } }); </script>
②. 2.6.0 + 使用作用域插槽
-
绑定在<slot> 元素上的 attribute 被称为插槽 prop。
-
具名作用域插槽
<div id="app"> <cpn> <!-- 2.通过v-slot获取到绑定在子组件上的bs数据,并存放在slotProps变量上 --> <!-- slotProps是自己命名的 --> <template v-slot:default="slotProps"> <ul> <li v-for="book in slotProps.bs">{{book}}</li> </ul> </template> <template v-slot:movieslot="slotMovies"> <ul> <li v-for="movie in slotMovies.ms">{{movie}}</li> </ul> </template> </cpn> </div> <template id="cpn"> <div> <!-- 1.将子组件的books数据绑定在自定义的bs变量上 --> <slot :bs="books"></slot> <slot name="movieslot" :ms="movies"></slot> </div> </template> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> const cpn = { template: '#cpn', data() { return { books: ['C语言编程', "C#一站式编程", "C指针", "PHP 基础"], movies: ['泰坦尼克号', '红日', '钢铁侠', '建党伟业'] } } } const vm = new Vue({ el: "#app", components: { cpn } }); </script>
[5]. 动态插槽名
- 下边的代码运行有问题,仅供参考
- 在 vue-cli 中使用动态插槽名是可以的
<div id="app"> {{slotName}} <cpn> <template v-slot:[slotName]> <span style="color:red;">一个新值</span> </template> </cpn> <button @click="checkSlotName">切换插槽名称</button> </div> <template id="soncpn"> <div> <slot name="left"><span>左边</span></slot> <slot name="middle"><span>中间</span></slot> <slot name="right"><span>右边</span></slot> <slot><p>被替换</p></slot> </div> </template> <script src="https://lib.baomitu.com/vue/2.7.7/vue.js"></script> <script> const cpn = { template: '#soncpn', data() { return { } } }; new Vue({ el: "#app", data: { count: 0, slotNameList: ['default', 'left', 'middle', 'right'] }, components: { cpn }, methods: { checkSlotName() { this.count++ } }, computed: { slotName() { return this.slotNameList[this.count % 4] } } }); </script>
[6]. 混入 mixin
https://cn.vuejs.org/v2/guide/mixins.html#%E5%9F%BA%E7%A1%80
-
mixin用来更高效的实现组件内容的复用。
-
一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
-
全局混合(所有的组件自动添加混合中的内容):Vue.mixin();
-
Mixin 的合并规则
-
情况一: 如果是 data 函数的返回值对象
- 返回值对象默认情况下会进行合并;
- 如果 data 返回值对象的的属性发生了冲突,那么会保留组件自身的数据
-
情况二: 如果是生命周期钩子函数:
- 生命周期的钩子函数会被合并到数组中,都会被调用
-
情况三: 值为对象的选项,例如 methods、components 和 directives, 将被合并为同一个对象
- 比如都有 methods 选项,并且都定义了方法,那么他们都会生效;
- 但是如果对象的key相同,那么会取组件对象的键值对
-
-
mixin并不是完美的解决方案,会有一些问题:
- 变量来源不明确,不利于阅读
- 多mixin可能会造成命名冲突
- mixin和组件可能出现多对多的关系,复杂度较高。
<div id="app">
<cmp></cmp>
<cmp2></cmp2>
</div>
<template id="cmp">
<div>
<ul>
<li v-for="item in arr">
{{item}}
</li>
</ul>
<ul>
<li v-for="(item,key) in obj">
{{key}}:{{item}}
</li>
</ul>
<p> {{foo}}</p>
<p> {{bar}}</p>
<hr>
</div>
</template>
<template id="cmp2">
<div>
<p>{{global}}</p>
<ul>
<li v-for="item in arr">
{{item}}
</li>
</ul>
<ul>
<li v-for="(item,key) in obj">
{{key}}:{{item}}
</li>
</ul>
<p>{{foo}}</p>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
// 全局混入
Vue.mixin({
data() {
return {
global: '全局mixin'
}
},
mounted() {
console.log('global mounted 被触发');
}
})
//定义一个局部混入对象
var myMixin = {
data() {
return {
arr: ['数据mix', '数据mix2'], //被覆盖掉
obj: { //进行混合
id: 1,
msg: '对象mix' //被覆盖掉
},
foo: 'abc'
}
},
methods: {
hello: function () {
console.log('hello from mixin!')
},
message() { //被覆盖掉了
console.log('mixin message');
}
},
mounted: function () { //被同时调用
this.hello();
this.message();
},
}
const cmp = {
template: '#cmp',
mixins: [myMixin], //可以添加多个局部mixin,会自动合并起来
data() {
return {
arr: ['数据1'],
obj: {
name: '对象',
msg: '对象app'
},
bar: 'def'
}
},
methods: {
message() {
console.log('message');
}
},
mounted() { //被同时调用
this.message();
console.log(this.$data);
}
};
const cmp2 = {
template: '#cmp2',
mixins: [myMixin], //可以添加多个局部mixin,会自动合并起来
};
const vm = new Vue({
el: "#app",
components:{cmp, cmp2}
});
</script>
(三) 生命周期
[1]. 单个组件
https://www.jianshu.com/p/410b6099be69
vm指vue实例let vm = new Vue({...})
①. 挂载阶段
从beforeCreate 、created、beforeMount、mounted
②. 更新阶段
beforeUpdate和updated
③. 销毁阶段
beforeDestroy和destroyed
[2]. 父子组件
①. 挂载阶段
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
②. 子组件更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated
③. 父组件更新过程
父beforeUpdate->父updated
④. 父组件销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed