目录
生命周期/生命周期回调函数/生命周期函数/生命周期钩子
一、介绍:
- 是什么:Vue在关键时刻帮我们调用的一些特殊名称的函数。
- 生命周期函数的名字不可更改,但函数的具体内容是程序员根据需求编写的。
- 生命周期函数中的this指向是vm或组件实例对象。
二、new Vue之后做了什么
5. 初始化生命周期、事件(其实就是制定一些规则,如生命周期有哪些?叫什么?什么时候去调用?事件修饰符都是干啥的?)。但数据代理未开始,即new Vue时传入的data还没有出现在vm身上,vm连_data都还没有
6. 调用beforeCreate(),此时无法通过vm访问到data中的数据、methods中的方法。很正常,此时数据代理还没开始,vm连_data都还没有
7. 初始化数据监测(vue如何检测对象变化、数组变化,给对象中的属性匹配getter和setter,对操作数组的方法进行二次包装)、数据代理。此时有vm上有_data了
8. 调用created(),此时可以通过vm访问到data中的数据、methods中的方法。
注意:beforeCreate()和created()指的是数据监测和数据代理创建前、创建完成
- 判断new Vue时是否传入el配置项?(此阶段vue开始解析模板(解析插值语法,@click、计算属性等),生成虚拟DOM保存在内存中,页面还不能显示解析好的内容(这个东西还没有变成真实DOM,还没有往页面上放),此时展示的是body标签中原始的未被vue解析内容,但是展示过程非常非常短,vue很快就接管root容器,编译模板,并呈现在页面上,所以未被vue解析的内容我们肉眼看不到
(1)传了el配置项,判断new Vue时是否传入template配置项?
(a)传了template配置项,直接去判断new Vue时是否传入template配置项。
(b)没传template配置项,编译el的外部HTML就作为template,意思是root容器作为template,如果他说内部HTML作为template,就是指root容器中的内容作为template,二者的区别:<div id="root" :x="n"></div>
,
如果将root容器作为template,会解析root容器的:x得到<div id="root" x="1"></div>
,
如果将root容器中的内容作为template,不会解析root容器的:x,上述代码不变
(2)没传el配置项,那么当vm.$mount(el)
调用后,才去判断new Vue时是否传入template配置项。如果没传el配置项,也没调用vm.$mount(el)
,那么后面的步骤都不进行了
判断new Vue时是否传入template配置项?
(a)传了template配置项,将template的内容编译为render函数
(b)没传template配置项,编译el的外部HTML就作为template,意思是root容器作为template,如果他说内部HTML作为template,就是指root容器中的内容作为template,二者的区别:<div id="root" :x="n"></div>
如果将root容器作为template,会解析root容器的:x得到<div id="root" x="1"></div>
如果将root容器中的内容作为template,不会解析root容器的:x,上述代码不变
- 调用beforeMount(),此时页面呈现的是未经vue编译的DOM结构,所有对DOM的操作都不奏效,因为在下一步,他始终是将上一步生成的虚拟DOM转成真实DOM,你在这里不管怎么改,他转换的都是上一步的虚拟DOM
- 创建
vm.$el
并替换el(el指root容器中的东西),在这一步,vue把刚才生成的虚拟DOM转成真实DOM,并用vm.$el
保存了一份真实DOM
为什么要创建
vm.$el
并保存一份真实DOM:vue在进行新旧虚拟DOM比对时,万一有的元素/节点可以复用,那他必须有之前的元素/节点,他才能去复用,
- 调用mounted(),此时页面中呈现的是经过vue编译的DOM,对DOM的操作均有效(但尽可能避免)。至此初始化过程结束。
一般在此阶段进行:开启定时器、发送网络请求、订阅消息、绑定自定义事件等初始化操作
如果你在mounted()中操作DOM,就有一种感觉:你让vue先工作,生成虚拟DOM再转换成真实DOM,再把真实DOM挂在页面上了,人家工作完了,你反手一改,人家白忙活了,所以要尽可能避免对DOM的操作
以上就是挂载流程,不涉及新旧虚拟DOM比对,因为挂载时压根没有旧的虚拟DOM
- 当data中的数据改变时,vue调用beforeUpdate(),此时数据是新的,但页面时旧的,即:页面尚未和数据保持同步
- 虚拟DOM重新渲染,新旧虚拟DOM进行比对,也就是说,在这个阶段会根据新数据,生成新的虚拟DOM,随后与旧的虚拟DOM进行比较,最终完成页面的更新,即完成了model->view的更新
- 调用updated(),此时数据和页面都是新的,即数据和页面保持同步
以上就是更新流程
- 当
vm.$destroy()
被调用时,调用beforeDestroy(),此时vm中所有的data、methods、指令等等,都处于可用状态,马上要执行销毁过程,但是你在beforeDestroy()中对数据做的所有操作都不会触发更新。
一般在此阶段:关闭定时器、取消订阅消息、解绑自定义事件等收尾操作
- 移除所有的watchers(监视)、子组件、事件监听器
- 调用destroyed()
当
vm.$destroy()
被调用时,vm就会自己销毁自己,会解绑全部指令和【自定义】事件监听器,但是vm之前的工作成果还在,只是之后没人帮你管理了
以上就是销毁流程
三、关于销毁Vue实例
- 销毁后借助Vue开发者工具看不到任何信息。
- 销毁后自定义事件会失效,但原生DOM事件依然有效。
- 一般不会在beforeDestroy操作数据,因为即便操作数据,也不会再触发更新流程了。
new Vue的template配置项
template的值是个字符串,把root容器中的内容全写在这个字符串里(注意:template只能有一个根节点,所以root容器中的内容要被包在一个div标签中,注意不能包在template标签中,因为template标签不能作为组件根元素,因为template标签中可能包含多个节点),配置了下面这个template,就不用在root容器中写代码了。但是这种写法,它会用template中的内容完全替换掉root容器,也就是说body标签中的内容就变成了template中的内容,没有<div id="root" :x="n"></div>
了,那么这样,你原本配置的:x="n"
也就没有了。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>分析生命周期</title>
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<!-- 准备好一个容器-->
<div id="root" :x="n"></div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
new Vue({
el:'#root',
template:`
<div>
<h2>当前的n值是:{{n}}</h2>
<button @click="add">点我n+1</button>
</div>
`,
data:{
n:1
},
methods: {
add(){
console.log('add')
this.n++
}
}
})
</script>
</html>
组件
一、组件:实现应用中局部功能代码和资源的集合
二、组件分为:非单文件组件、单文件组件
- 非单文件组件:一个文件中包含n个组件,缺点:样式不能跟着组件走
- 单文件组件:一个文件中只包含1个组件
三、Vue中使用组件的三大步骤:
- 定义组件(创建组件)
使用Vue.extend(options)
创建,其中options
和new Vue(options)
时传入的那个options
几乎一样,但也有点区别,区别如下:
(1)el不要写,因为最终所有的组件都要经过一个vm的管理,由vm中的el决定服务哪个容器。
(2)data必须写成函数,为了避免组件被复用时,数据存在引用关系:如果data是对象形式,那么想象一下这样一个场景:你定义了一个组件,组件中data是对象,a页面用了该组件,b页面也用了该组件,a页面改了data中的数据,那么b页面的数据也被改了,因为data是个对象,data中存放的是引用地址。把data定义为函数,就可以防止一个组件被多次使用时,存在数据的引用关系。每次获取数据时,都现场调用data函数,拿data函数的返回值给你用
备注:使用template可以配置组件结构。
- 注册组件
(1)局部注册:靠new Vue
的时候传入components
选项
(2)全局注册:靠Vue.component('组件名',组件)
- 使用组件(写组件标签):
<school></school>
非单文件组件
基本使用
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>基本使用</title>
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<div id="root">
<!-- 第三步:编写组件标签 -->
<hello></hello>
<hr>
<h1>{{msg}}</h1>
<hr>
<!-- 第三步:编写组件标签 -->
<school></school>
</div>
<div id="root2">
<!-- 所有的vm都可以使用全局注册的组件 -->
<!-- 第三步:编写组件标签 -->
<hello></hello>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false
//第一步:创建school组件:Vue.extend({配置对象})
const s = Vue.extend({
name:'atguigu',//在开发者工具中的名字
// 这个template和我们之前说的new Vue中的template配置项的用法一致
template:`
<div class="demo">
<h2>学校名称:{{schoolName}}</h2>
<h2>学校地址:{{address}}</h2>
<button @click="showName">点我提示学校名</button>
</div>
`,
// el:'#root', //组件定义时,一定不要写el配置项,因为最终所有的组件都要被一个vm管理,由vm决定服务于哪个容器。
data(){// data一定要写成函数形式,并且是普通函数
return {
schoolName:'尚硅谷',
address:'北京昌平'
}
},
methods: {
showName(){
alert(this.schoolName)
}
},
})
//第一步:创建hello组件
const hello = Vue.extend({
template:`
<div>
<h2>你好啊!{{name}}</h2>
</div>
`,
data(){
return {
name:'Tom'
}
}
})
//第二步:全局注册组件:Vue.component('组件真正叫的名字',组件所在的变量名)
Vue.component('hello',hello)// 所有的vm都能用这个组件
//创建vm
new Vue({
el:'#root',
data:{
msg:'你好啊!'
},
//第二步:注册组件(局部注册)
components:{
// 这里面写键值对,键是组件真正叫的名字,值是你上面第一步定义的变量名(组件所在变量)
school: s,// 也就是说键名和你使用组件的标签名保持一致
}
})
new Vue({
el:'#root2',
})
</script>
</html>
几个注意点:
- 关于组件名:
(1)一个单词组成:
第一种写法(首字母小写):school
第二种写法(首字母大写):School
(2)多个单词组成:
第一种写法(kebab-case命名):my-school,注意:注册时键名有-,要使用’‘包裹,即’my-school’
第二种写法(CamelCase命名):MySchool(需要Vue脚手架支持)
备注:
- 组件名尽可能回避HTML中已有的元素名称,例如:h2、H2都不行。
- 可以使用name配置项指定组件在开发者工具中呈现的名字。
- 关于组件标签:
第一种写法:<school></school>
第二种写法:<school/>
备注:不用使用脚手架时,
<school/>
会导致后续组件不能渲染。
- 一个简写方式:
const school = Vue.extend(options)
可简写为:const school = options
,对于简写方式,vue底层有个判断,在注册组件时,如果收到的是个对象,那么他会把这个对象作为参数传入Vue.extend()
,表面上你虽然没有调用Vue.extend()
,但实际底层vue帮你调用了。如果不是简写,那么在注册组件时,vue直接把Vue.extend(options)
拿过来用
组件的嵌套
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>组件的嵌套</title>
<!-- 引入Vue -->
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<!-- 准备好一个容器-->
<div id="root"></div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
//定义student组件。school组件是student组件的父组件,注意要把student组件的定义放在school组件前面
//意思是:把子组件准备(定义)好了再去父组件那里注册
const student = Vue.extend({
name:'student',
template:`
<div>
<h2>学生姓名:{{name}}</h2>
<h2>学生年龄:{{age}}</h2>
</div>
`,
data(){
return {
name:'尚硅谷',
age:18
}
}
})
//定义school组件
const school = Vue.extend({
name:'school',
template:`
<div>
<h2>学校名称:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
<student></student>
</div>
`,
data(){
return {
name:'尚硅谷',
address:'北京'
}
},
//注册组件(局部)。school组件是student组件的父组件,那么把student组件注册到school组件中。
//并且student组件只能在school组件中使用
components:{
student
}
})
//定义hello组件
const hello = Vue.extend({
template:`<h1>{{msg}}</h1>`,
data(){
return {
msg:'欢迎来到尚硅谷学习!'
}
}
})
//定义app组件
/*
开发中会定义个app组件,用于管理应用中所有的组件,vm管理app组件,app组件管理别的组件
*/
const app = Vue.extend({
template:`
<div>
<hello></hello>
<school></school>
</div>
`,
components:{
school,
hello
}
})
//创建vm
new Vue({
template:'<app></app>',
el:'#root',
//注册组件(局部)
components:{app}
})
</script>
</html>
VueComponent
- school组件本质是一个名为
VueComponent
的构造函数,且不是程序员定义的,是Vue.extend
生成的。 - 我们只需要写
<school/>
或<school></school>
,Vue解析时会帮我们创建school组件的实例对象,即Vue帮我们执行的:new VueComponent(options)
。 - 特别注意:每次调用
Vue.extend
,返回的都是一个全新的VueComponent!!!! - 关于this指向:
(1)组件配置中:data函数、methods中的函数、watch中的函数、computed中的函数,它们的this均是【VueComponent实例对象】。
(2)new Vue(options)
配置中:data函数、methods中的函数、watch中的函数、computed中的函数,它们的this均是【Vue实例对象】。 - VueComponent的实例对象,以后简称vc(也可称之为:组件实例对象)。Vue的实例对象,以后简称vm。
- 组件是可复用的vue实例,所以他和
new Vue
接收相同的选项,仅有的例外是像el这样根实例特有的选项。也就是说,vc有的功能,vm都有,vm有的功能(能通过el决定为哪个容器服务)vc就没有 - 一个重要的内置关系:
VueComponent.prototype.__proto__ === Vue.prototype
,也就是说,vue内部将VueComponent.prototype.__proto__
指向了Vue.prototype
为什么要有这个关系:让组件实例对象(vc)可以访问到 Vue原型上的属性、方法。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>VueComponent</title>
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<!-- 准备好一个容器-->
<div id="root">
<school></school>
<hello></hello>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false
//定义school组件。Vue.extend()的返回值是个VueComponent函数
const school = Vue.extend({
name:'school',
template:`
<div>
<h2>学校名称:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
<button @click="showName">点我提示学校名</button>
</div>
`,
data(){//data中的数据通过数据代理放在了VueComponent实例对象身上
return {
name:'尚硅谷',
address:'北京'
}
},
methods: {
showName(){
console.log('showName',this)// this指向VueComponent实例对象,
// 打印出来的VueComponent实例对象的内容和vue实例对象的一样
// 区别在于:1.vue实例对象有el,VueComponent实例对象没有,
// 2.vue实例对象的data是对象,VueComponent实例对象的data是函数,
// 3.vue实例对象使用new Vue()创建,VueComponent实例对象使用new VueComponent()创建的
}
},
})
const test = Vue.extend({
template:`<span>atguigu</span>`
})
//定义hello组件
const hello = Vue.extend({
template:`
<div>
<h2>{{msg}}</h2>
<test></test>
</div>
`,
data(){
return {
msg:'你好啊!'
}
},
components:{test}
})
// console.log('@',school)
// console.log('#',hello)
//创建vm
const vm = new Vue({
el:'#root',
components:{school,hello}
})
</script>
</html>
单文件组件
main.js作为入口文件,在此创建vue实例,管控组件leader
import App from './App.vue'
new Vue({
el:'#root',//vue接管root容器
template:`<App></App>`,
components:{App},
})
vue接管root容器,所以还需要新建index.html文件,index.html文件里面存放root容器:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>练习一下单文件组件的语法</title>
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<!-- 准备一个容器 -->
<div id="root"></div>
</body>
</html>
单文件组件都以.vue为后缀,浏览器不能直接运行.vue文件,我们需要对其处理,让他变成最纯粹的js文件,浏览器就认识了。如何处理:1.自己使用webpack搭建一个工作流;2.vue脚手架
xx.vue命名规则和组件名命名规则一致
.vue文件中不能new Vue()
,因为.vue文件是组件,组件不是new Vue()
来的
组件:实现应用中局部功能代码和资源的集合。那么组件应该有js、HTML、css这三个结构,为此.vue文件设置了三个标签来分别存放这些内容
<template>组件的结构</template>
<script>组件交互相关代码</script>
<style>组件的样式</style>
我们之前定义组件时,是这样写的:
const school = Vue.extend({
template:`// template中的内容放在template标签中
<div class="demo">
<h2>学校名称:{{schoolName}}</h2>
<h2>学校地址:{{address}}</h2>
<button @click="showName">点我提示学校名</button>
</div>
`,
// 下面的都属于交互内容,放在script标签中
data(){
return {
schoolName:'尚硅谷',
address:'北京昌平'
}
},
methods: {
showName(){
alert(this.schoolName)
}
},
})
把这部分内容分别写在对应的标签中即可
<template>
<!-- template标签中必须有个根元素 -->
<div class="demo">
<h2>学校名称:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
<button @click="showName">点我提示学校名</button>
</div>
</template>
<script>
export default {
name:'School',
data(){
return {
name:'尚硅谷',
address:'北京昌平'
}
},
methods: {
showName(){
alert(this.name)
}
},
}
</script>
<style>
.demo{
background-color: orange;
}
</style>
app组件用于汇总所有组件,相当于组件的leader,app组件由vm管控,因为.vue文件是组件,且不能被浏览器识别,所以我们需要新建main.js文件,在main.js中创建vue实例vm,让vm管控app组件
<template>
<div>
<!-- 3.使用组件 -->
<School></School>
<Student></Student>
</div>
</template>
<script>
//1.引入组件
import School from './School.vue'
export default {
name:'App',
//2.注册组件
components:{
School
}
}
</script>
vue脚手架vue-cli
安装与使用
- (仅第一次执行)全局安装@vue/cli:
npm install -g @vue/cli
,安装好了之后,就多了个vue
命令
如果下载缓慢请配置npm淘宝镜像:
npm config set registry https://registry.npm.taobao.org
- 切换到你要创建项目的目录,然后使用命令创建项目:
vue create xxx
本项目使用vue2
- 启动项目:
npm run serve
,执行该命令后,vue会直接运行main.js
脚手架结构
.gitignore:git的忽略文件,哪些文件不想被git管理,在这里配好
babel.config.js:Babel的控制文件
package-lock.json和package.json:只要你打开的工程符合npm规范,那么一定有这两个文件,他们是包的说明书,前者是包版本控制/锁定文件。指令介绍:
"scripts": {
"serve": "vue-cli-service serve",/*开发时使用这个命令,让别人帮你配置服务器,帮你把东西都弄好*/
"build": "vue-cli-service build",/*代码写完了,功能开发完了,想把整个项目变成浏览器认识的文件*/
"lint": "vue-cli-service lint"/*对.js和.vue文件进行语法检查*/
},
render函数
main.js作为入口文件,我们在里面创建vue实例时,写的以下代码:
new Vue({
el:'#app',
render: h => h(App)
})
我们以前创建vue实例时,写的代码如下,如果我们还是写下面这个代码会报错,这是为什么呢?
new Vue({
el:'#app',
template:`<h1>你好啊</h1>`,
components:{App},
})
因为我们引入vue(import Vue from 'vue'
)使用的是ES6模块化引入,此时引入的是..\node_modules\vue\dist\vue.runtime.esm.js
,该vue残缺了模板解析器,意味着没人给你解析new Vue()中的template配置项,你必须使用render函数。
完整版的vue是
..\node_modules\vue\dist\vue.js
。
如果你引入完整版的vue(import Vue from 'vue/dist/vue.js'
),在new Vue()时可以配置template,不用配置render。
引入残缺版vue,但是我还想配置内容,就要使用render函数,render函数是vue帮你调用的,render函数将App组件放入容器中,render函数必须有返回值,返回值就是你想渲染的具体内容,render函数接收一个参数createElement,该参数createElement是个函数,借助该函数createElement渲染具体的内容。createElement('标签名','标签体内容')
或createElement(组件名)
render(createElement){
return createElement('h1','你好啊')
}
没有用到this,可以写成箭头函数,箭头函数只有一个参数,可以省略(),只有一条语句并且是返回值,可以省略return和{},createElement是形参,可以用其他变量代替,如h,那么上面代码可以精简为:render: h => h('h1','你好啊')
。等同于:template':<h1>你好啊</h1>'
综上:
- 如果你引入的时残缺版的vue,因为vue.runtime.xxx.js没有模板解析器,所以不能使用template配置项,需要使用render函数接收到的createElement函数去指定具体内容。那么新建vue实例时代码如下:(推荐)
import Vue from 'vue'
import App from './App.vue'//引入App组件,它是所有组件的父组件
new Vue({
el:'#app',
render: h => h(App),// 这个App是个变量,他会去上面找,就找到了你上面引入的那个组件
})
- 如果你引入的是完整版的vue,那么新建vue实例时代码如下:
import Vue from 'vue/dist/vue.js'
import App from './App.vue'//引入App组件,它是所有组件的父组件
new Vue({
el:'#app',
template:`<h1>你好啊</h1>`,
components:{App},
})
完整版vue.js与残缺版vue.runtime.xxx.js的区别:
- vue.js是完整版的Vue,包含:核心功能(声明周期、处理事件等功能)+模板解析器(解析new Vue()时传入的template配置项)。
- vue.runtime.xxx.js是运行版的Vue,只包含:核心功能;没有模板解析器。
为什么要设计精简版vue:如果只有完整版vue的话,在vue源码中,模板解析器代码的体积占了vue的1/3,有天你代码写完了,需要交给webpack打包,webpack打包完了,会生成一个非常大的文件,该文件中肯定包含vue,也就包含模板解析器的代码,但此时乃至以后,都用不上模板解析器了,因为在开发时,模板解析器帮我们翻译模板,但现在我们的代码写完了,借助webpack已经可以把vue翻译成.js、.css、.html了,而且那些该解析的模板都解析完了,变成了浏览器认识的文件,此时模板解析器就没什么作用了,但是他还在打包后的文件中。所以就出现了精简版vue,打包后的文件能节约一些空间。
举一个生活中的例子:假如你家要装修,要铺瓷砖,你有以下两种方案:
- 买瓷砖(vue核心)+买工人(模板解析器),瓷砖铺好后你得到了铺好的瓷砖和工人,你以后还要养着他们,没必要啊,他们以后没用了
- 买瓷砖(vue核心)+雇工人(模板解析器),瓷砖铺好后你得到了铺好的瓷砖
修改脚手架的默认配置
Vue脚手架隐藏了所有webpack相关的配置,若想查看具体的webpack配置,请执行:vue inspect > output.js
如果你采用脚手架的默认配置,以下文件/文件夹名你不能改:public、index.html、src、main.js。但是如果你非要改,在package.json保存的地方新建vue.config.js(他们两个文件要同级),vue.config.js代码如下:
//vue最终会把vue.config.js输送给webpack,webpack是基于Node的,所以vue.config.js中使用commonJS。
module.exports = {
pages: {
index: {
entry: 'src/main.js',//入口
},
},
lintOnSave:false, //关闭语法检查
}
修改package.json后一定要重新npm run serve
。
其余配置参考官方配置参考。
其实,脚手架会把vue.config.js中的配置与webpack中已经写好的配置进行合并,某个配置如果vue.config.js中有,则使用vue.config.js的,否则使用webpack中已经写好的,这样程序员是碰不到核心文件的。
ref属性
需求:点击按钮获取某个DOM元素。
vue给我们提供了ref属性,用于给元素或子组件注册引用信息(id的替代者),即给DOM节点打标识。用在html标签上获取的是真实DOM元素,应用在组件标签上是组件实例对象(vc)
用法:
- 打标识:
<h1 ref="xxx">.....</h1> 或 <School ref="xxx"></School>
- 获取:
this.$refs.xxx
VueComponent会把所有包含ref的节点收集到自身的$refs
属性中,如下图。$refs
的值是一个对象,键是ref的值,值是该ref的节点。
app.vue中代码如下:
<template>
<div>
<h1 v-text="msg" ref="title"></h1>
<button ref="btn" @click="showDOM">点我输出上方的DOM元素</button>
<School ref="sch"/>
<School id="sch"/>
</div>
</template>
<script>
//引入School组件
import School from './components/School'
export default {
name:'App',
components:{School},
data() {
return {
msg:'欢迎学习Vue!'
}
},
methods: {
showDOM(){
console.log(this.$refs.title) //真实DOM元素
console.log(this.$refs.btn) //真实DOM元素
console.log(this.$refs.sch) //School组件的实例对象VueComponent(vc)
console.log(document.getElementById('sch'))//School组件对应的完整的dom结构
}
},
}
</script>
school.vue中代码如下:
<template>
<div class="school">
<h2>学校名称:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
</div>
</template>
<script>
export default {
name:'School',
data() {
return {
name:'尚硅谷',
address:'北京·昌平'
}
},
}
</script>
<style>
.school{
background-color: gray;
}
</style>
使用ref和使用id的区别:对于传统的HTML标签来说,二者只有写法和用法上的区别,但是对于组件标签来说,<School ref="sch"/>
得到的是School组件的实例对象VueComponent。如下图
<School id="sch"/>
得到的是School组件对应的完整的dom结构,如下图
props配置
props:让组件接收外部传过来的数据。
父组件给子组件传数据:通过给子组件的标签写属性的方式传入数据:数据名="值"
,一定要加""
app.vue代码:
<template>
<div>
<!--:age="18":age的值是运行""中的内容得到的结果,就是数字18,""中的内容当成了表达式执行-->
<Student name="李四" sex="女" :age="18"/>
</div>
</template>
<script>
import Student from './components/Student'
export default {
name:'App',
components:{Student}
}
</script>
子组件使用props接收父组件传入的数据,使用props接收到的数据保存在了本组件实例对象身上,如果你在子组件实例对象身上的props中声明了一个变量,但是父组件没给你传该变量,那么多余变量的值为undefined
⚠️props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据。
Student.vue中代码:
<template>
<div>
<!-- 在这里你相当于就在组件实例对象上了,可以使用组件实例对象身上的所有东西 -->
<h1>{{msg}}</h1>
<h2>学生姓名:{{name}}</h2>
<h2>学生性别:{{sex}}</h2>
<h2>学生年龄:{{myAge+1}}</h2>
<button @click="updateAge">尝试修改收到的年龄</button>
</div>
</template>
<script>
export default {
name:'Student',
data() {
console.log(this)
return {
msg:'我是一个尚硅谷的学生',
// 接收到的props不允许改,控制台会报错,但是还是能修改成功.那么如果我们想修改接收到的数据,
// 可以在data中定义个变量接收接收到的数据,后续改data中的变量即可
myAge:this.age
// name:'cara'//在data中定义个name变量,在props中定义个name变量用于接收父组件传过来的数据
// 两个name都会被放到组件实例对象上,会发生冲突,控制台会报错,props的name变量优先级更高,
// 以外部传进来的数据为主,也就是说props的变量优先被组件实例接收,优先被放在组件实例身上
}
},
methods: {
updateAge(){
this.myAge++
}
},
//如果你这里声明了一个变量,但是父组件没给你传,那么多余变量的值为undefined
// props:['name','age','sex']
//接收的同时对数据进行类型限制,props写成对象,里面是一对对的键值对,键是数据名,值是JS内置对象
/* props:{
name:String,
age:Number,
sex:String
} */
//接收的同时对数据:进行类型限制+默认值的指定+必要性的限制
props:{
//required和default不能同时存在
name:{
type:String, //name的类型是字符串
required:true, //name是必要的
},
age:{
type:Number,
default:99 //默认值
},
sex:{
type:String,
required:true
}
}
}
</script>
mixin(混入)
混入:可以把多个组件共用的配置提取成一个混入对象
需求:点击“姓名”,弹窗弹出姓名。
现在我们有两个组件School和Student,他们中间有些代码一样
那我们可以把这些一样的代码提取到一个文件中,在School和Student使用混入直接使用这些相同的代码。新建mixin.js,将相同代码提取并暴露。注意:此处为了后续演示,我们还添加了生命周期函数和data。
export const hunhe = {
methods: {
showName(){
alert(this.name)
}
},
mounted() {
console.log('你好啊!')
},
}
export const hunhe2 = {
data() {
return {
x:100,
y:200
}
},
}
混入原则:
- 对于data中的数据、methods中的方法,混合中有的,你这里也有,那就使用你这里的,混合中有的,你这里没有,那就是用混合中的
- 对于生命周期函数,混合中有的和你这里有的都会用,并且优先使用混合中的。
有两种使用混入的方式:
- 在【用到混入的组件中引入并配置mixins】即可,如果只有Student组件使用,Student组件代码如下
<template>
<div>
<h2 @click="showName">学生姓名:{{name}}</h2>
<h2>学生性别:{{sex}}</h2>
</div>
</template>
<script>
import {hunhe,hunhe2} from '../mixin'//局部引入一个hunhe
export default {
name:'Student',
data() {
return {
name:'张三',
sex:'男'
}
},
mixins:[hunhe,hunhe2]
}
</script>
- 如果有很多组件乃至所有的组件都要用到混入中的代码,那么在main.js中全局引入并注册混入,main.js中代码:
import Vue from 'vue'//引入Vue
import App from './App.vue'//引入App
import {hunhe,hunhe2} from './mixin'//全局引入混合
//这样写,在你整个应用中,所有VC和VM都会得到这两个混合
Vue.mixin(hunhe)
Vue.mixin(hunhe2)
new Vue({//创建vm
el:'#app',
render: h => h(App)
})
插件
在plugins.js中定义一个插件plugins,用于增强vue。插件的本质是对象,这个对象里面必须有install函数,install函数收到的第一个参数是vue构造函数Vue,之后的参数就是插件使用者传递进来的数据。
export default {
install(Vue,x,y,z){
console.log(x,y,z)
//全局过滤器
Vue.filter('mySlice',function(value){
return value.slice(0,4)
})
//定义全局指令
Vue.directive('fbind',{
//指令与元素成功绑定时(一上来)
bind(element,binding){
element.value = binding.value
},
//指令所在元素被插入页面时
inserted(element,binding){
element.focus()
},
//指令所在的模板被重新解析时
update(element,binding){
element.value = binding.value
}
})
//定义混入
Vue.mixin({
data() {
return {
x:100,
y:200
}
},
})
//给Vue原型上添加一个方法(vm和vc就都能用了)
Vue.prototype.hello = ()=>{alert('你好啊')}
}
}
使用插件时,在main.js中先引入再使用Vue.use()
应用插件,vue会帮你调用插件的install方法,并把vue构造函数作为第一个参数传入install方法。
import plugins from './plugins'//引入插件
//应用(使用)插件,一定要先应用插件,再创建vm.
Vue.use(plugins,1,2,3)
然后就可以使用插件了,如在组件中使用:
<template>
<div>
<h2>学校名称:{{name | mySlice}}</h2>
<input type="text" v-fbind:value="name">
<button @click="test">点我测试一个hello方法</button>
</div>
</template>
<script>
export default {
name:'School',
data() {
return {
name:'尚硅谷atguigu'
}
},
methods: {
test(){
this.hello()
}
},
}
</script>
scoped样式
以前我们写style样式时,没有加scoped,此时在每个组件中写的样式都汇总在一起了,可能出现类名冲突,在这种情况下,后【import】引入的组件的样式会覆盖掉先引入的组件的样式。为了解决这个问题,style标签添加scoped,使得样式只在【本组件】起作用,不会在子组件起作用。
写法:<style scoped>
原理:给style标签添加scoped,vue会给最外层的div添加一定标签属性,属性的值时随机生成的,再配合标签属性选择器,就完成了控制指定的div的样式
⚠️脚手架解析vue文件时,先引入,再读取配置项,最后才解析模板。
style标签中还有lang属性,用于指定写样式的语言(不写这个属性默认指定css),如css、scss、less。但是vue脚手架无法处理css之外的样式语言,所以需要提前安装插件,如less-loader,但是可能出现兼容性问题,建议安装:npm i less-loader@7
在终端输入:
npm view less-loader versions
可以查看less-loader的【所有】版本
总结TodoList案例
一、组件化编码流程:
- 拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。
- 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
(1)一个组件在用:放在组件自身即可。
(2)一些组件在用:放在他们共同的父组件上(状态提升)。
(3)实现交互:从绑定事件开始。
二、props适用于:
- 父组件 => 子组件 通信
- 子组件 => 父组件 通信(子组件给父组件传数据:父组件定义个方法,方法的形参用于接收子组件传过来的数据,然后把这个方法传给子组件。子组件通过props接收到该方法,然后调用,并把数据作为实参传进去,这样,父组件该方法的形参就拿到了子组件的数据)
三、使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的!
四、props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。
五、数据在哪里,操作数据的方法就在哪里。
浏览器本地存储webStorage
一、存储内容大小一般支持5MB左右(不同浏览器可能还不一样)
二、浏览器端通过Window.sessionStorage
和Window.localStorage
属性来实现本地存储机制。
三、相关API:
xxxxxStorage.setItem('key', 'value')
:该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值。xxxxxStorage.getItem('person')
:该方法接受一个键名作为参数,返回键名对应的值。xxxxxStorage.removeItem('key')
:该方法接受一个键名作为参数,并把该键名从存储中删除。xxxxxStorage.clear()
:该方法会清空存储中的所有数据。
四、备注:
- SessionStorage存储的内容会随着浏览器窗口关闭而消失。
- LocalStorage存储的内容,需要手动清除才会消失。
xxxxxStorage.getItem(xxx)
如果xxx对应的value获取不到,那么getItem的返回值是null。JSON.parse(null)
的结果依然是null。
五、localstorage的应用:保存搜索记录
六、浏览器中【Session会话】指的是浏览器关闭
七、SessionStorage和LocalStorage的API一样,他们最大的区别就是前者会话结束内容消失,后者需要手动删除
八、LocalStorage存储的内容什么时候消失:
- 你引导用户点击了某个按钮,操作了
removeItem()
或clear()
- 用户自己在浏览器中清空缓存、清空数据
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>localStorage</title>
</head>
<body>
<h2>localStorage</h2>
<button onclick="saveData()">点我保存一个数据</button>
<button onclick="readData()">点我读取一个数据</button>
<button onclick="deleteData()">点我删除一个数据</button>
<button onclick="deleteAllData()">点我清空一个数据</button>
<script type="text/javascript" >
let p = {name:'张三',age:18}
function saveData(){
/*
localStorage.setItem('键','值')。键值都必须是字符串,如果值不是字符串,他会帮你调用值的toString()
如果值是个对象,调完toString()就变成了[object Object]。
使用JSON.stringify(字符串)将对象变成字符串
*/
localStorage.setItem('msg','hello!!!')
localStorage.setItem('msg2',666)// 666.toString()
localStorage.setItem('person',JSON.stringify(p))
}
function readData(){
// localStorage.getItem('键')获取键对应的值,获取到的值是字符串,获取不到则返回null
console.log(localStorage.getItem('msg'))
console.log(localStorage.getItem('msg2'))
const result = localStorage.getItem('person')
console.log(JSON.parse(result))// JSON.parse(result)将字符串转成对象。JSON.parse(null)为null
// console.log(localStorage.getItem('msg3'))// null
}
function deleteData(){
// localStorage.removeItem('键')删除键对应的键值对
localStorage.removeItem('msg2')
}
function deleteAllData(){
localStorage.clear()// 清空
}
</script>
</body>
</html>
自定义事件
一、内置事件是给HTML元素用的,自定义事件是给组件用的。
二、组件的自定义事件是一种组件间通信的方式,适用于:子组件 => 父组件
三、使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中)。
四、绑定自定义事件:
- 第一种方式,在父组件中:
<Demo @atguigu="test"/> 或 <Demo v-on:atguigu="test"/>
- 第二种方式,在父组件中:
<Demo ref="demo"/>
......
mounted(){
this.$refs.xxx.$on('atguigu',this.test)
}
五、若想让自定义事件只能触发一次,可以使用.once
修饰符,或$once
方法。
六、触发自定义事件:this.$emit('atguigu',数据)
七、解绑自定义事件this.$off('atguigu')
八、组件上也可以绑定原生DOM事件,需要使用.native
修饰符。
九、通过this.$refs.xxx.$on('atguigu',回调)
绑定自定义事件时,回调要么配置在methods中,要么用箭头函数,否则this指向会出问题!
绑定事件
需求:点击School组件的按钮,School组件将自己的数据name传给父组件App组件
思路:以前的做法(本组件代码):父组件给子组件传递一个函数,子组件调用该函数,并将数据作为参数传进去,父组件就能收到数据,在函数中进行相应的处理
App.vue中代码:
<template>
<div class="app">
<h1>{{msg}},学生姓名是:{{studentName}}</h1>
<!-- 通过父组件给子组件传递函数类型的props实现:子给父传递数据 -->
<School :getSchoolName="getSchoolName"/>
</div>
</template>
<script>
import School from './components/School'
export default {
name:'App',
components:{School},
data() {
return {
msg:'你好啊!',
studentName:''
}
},
methods: {
getSchoolName(name){
console.log('App收到了学校名:',name)
}
}
</script>
School.vue中代码:
<template>
<div>
<h2>学校名称:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
<button @click="sendSchoolName">把学校名给App</button>
</div>
</template>
<script>
export default {
name:'School',
props:['getSchoolName'],
data() {
return {
name:'尚硅谷',
address:'北京',
}
},
methods: {
sendSchoolName(){
this.getSchoolName(this.name)
}
},
}
</script>
需求:点击Student组件的按钮,Student组件将自己的数据name传给父组件App组件
- 方法一:通过父组件使用==@或v-on==给子组件绑定一个自定义事件实现
思路:子组件通过$emit
发送一个事件,数据放在事件的参数中,父组件使用@或v-on监听事件,监听到后调用某函数,数据会直接作为参数传入该函数
注意:@或v-on在谁身上,就是在给谁绑定事件
<Student @atguigu="getStudentName"/>
这行代码:@或v-on在Student这个组件标签上,所以是给Student这个组件的实例对象VueComponent身上绑定了一个事件,事件名叫atguigu,如果以后有人触发了该事件,那么getStudentName函数就会被调用。
App.vue中代码:
<template>
<div class="app">
<h1>{{msg}},学生姓名是:{{studentName}}</h1>
<!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(使用@或v-on)-->
<Student @atguigu="getStudentName"/>
<!-- 使用事件修饰符.once,让atguigu只有在第一次触发时会调用getStudentName -->
<!-- <Student @atguigu.once="getStudentName"/> -->
</div>
</template>
<script>
import Student from './components/Student'
export default {
name:'App',
components:{School,Student},
data() {
return {
msg:'你好啊!',
studentName:''
}
},
methods: {
// 在Student组件中触发atguigu事件时传入了参数,在getStudentName函数中会收到那些参数
// name,...params:第一个参数正常接收,剩余参数全部接收到数组params中
getStudentName(name,...params){
console.log('App收到了学生名:',name,params)
this.studentName = name
}
}
}
</script>
如何触发atguigu事件:你给谁绑定的事件,你就找谁去触发事件。Student.vue代码:
<template>
<div class="student">
<h2>学生姓名:{{name}}</h2>
<h2>学生性别:{{sex}}</h2>
<h2>当前求和为:{{number}}</h2>
<button @click="sendStudentlName">把学生名给App</button>
</div>
</template>
<script>
export default {
name:'Student',
data() {
return {
name:'张三',
sex:'男',
number:0
}
},
methods: {
sendStudentlName(){
//触发Student组件实例身上的atguigu事件:this.$emit('想触发的事件名',后面的参数可选且都是数据)
this.$emit('atguigu',this.name,666,888,900)
}
}
}
</script>
- 方法二:通过父组件使用ref给子组件绑定一个自定义事件实现
思路:App组件一挂载,就给子组件中ref为xxx的标签绑定事件,并指定监听到事件后触发什么函数,子组件通过$emit
发送一个事件,数据放在事件的参数中。
App.vue中代码:
<template>
<div class="app">
<h1>{{msg}},学生姓名是:{{studentName}}</h1>
<!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第二种写法,使用ref) -->
<Student ref="student"/>
</div>
</template>
<script>
import Student from './components/Student'
export default {
name:'App',
components:{School,Student},
data() {
return {
msg:'你好啊!',
studentName:''
}
},
methods: {
getStudentName(name,...params){
console.log('App收到了学生名:',name,params)
this.studentName = name
}
},
mounted() {
//下面这行代码:App组件一挂载,就给组件中ref为student的标签绑定atguigu事件,
//当atguigu事件被触发时,调用this.getStudentName方法
this.$refs.student.$on('atguigu',this.getStudentName) //绑定自定义事件
// this.$refs.student.$once('atguigu',this.getStudentName) //绑定自定义事件(一次性)
/*
如果把第一行代码改为:直接把回调函数作为第二个参数传进来,回调函数是普通函数
this.$refs.student.$on('atguigu',function(name,...params){
console.log('App收到了学生名:',name,params)
console.log(this)//this指向触发事件的组件,即绑定事件的实例对象VueComponent,这里为Student实例对象
//vue底层的设计:谁触发的事件,事件回调中的this就指向谁
this.studentName = name
})
但是如果代码写为:this.$refs.student.$on('atguigu',this.getStudentName)
Student组件实例对象触发的atguigu事件,所以this.getStudentName中的this本应该指向Student组件实例对象,
但是getStudentName写在了methods中,且getStudentName是普通函数,那么getStudentName中的this指向本组件实例对象,
这样一看貌似this冲突了,其实vue以后者为主,getStudentName中的this指向本组件实例对象,即App组件实例对象
如果代码写为:直接把回调函数作为第二个参数传进来,回调函数是箭头函数
this.$refs.student.$on('atguigu',(name,...params)=>{
console.log('App收到了学生名:',name,params)
console.log(this)//this指向本组件实例对象,即App组件实例对象
this.studentName = name
})
箭头函数没有this,箭头函数的this是定义箭头函数时,所在的作用域指向的对象,这里是mounted函数,
vue中规定,生命周期函数是普通函数,那么生命周期函数中的this指向本组件实例对象,即App组件实例对象
*/
},
}
</script>
如果把mounted中的第一行代码(this.$refs.student.$on('atguigu',this.getStudentName)
)改为如下,即直接把回调函数作为第二个参数传进来,回调函数是普通函数
this.$refs.student.$on('atguigu',function(name,...params){
console.log('App收到了学生名:',name,params)
console.log(this)//this指向触发事件的组件,即绑定事件的实例对象VueComponent,这里为Student实例对象
//vue底层的设计:谁触发的事件,事件回调中的this就指向谁
this.studentName = name
})
但是如果代码写为:this.$refs.student.$on('atguigu',this.getStudentName)
Student组件实例对象触发的atguigu事件,所以this.getStudentName中的this本应该指向Student组件实例对象,但是getStudentName写在了methods中,且getStudentName是普通函数,那么getStudentName中的this指向本组件实例对象,这样一看貌似this冲突了,其实vue以后者为主,getStudentName中的this指向本组件实例对象,即App组件实例对象
如果代码写为如下,即直接把回调函数作为第二个参数传进来,回调函数是箭头函数
this.$refs.student.$on('atguigu',(name,...params)=>{
console.log('App收到了学生名:',name,params)
console.log(this)//this指向本组件实例对象,即App组件实例对象
this.studentName = name
})
箭头函数没有this,箭头函数的this是定义箭头函数时,所在的作用域指向的对象,这里是mounted函数,vue中规定,生命周期函数是普通函数,那么生命周期函数中的this指向本组件实例对象,即App组件实例对象
方法二适用场景:你可以在组件挂载后5s或者等ajax请求的数据回来了,再给标签绑定事件,此时mounted中的方法改为如下:
setTimeout(() => {
this.$refs.student.$on('atguigu',this.getStudentName)
},5000)
方法一一上来就给标签绑定事件了,方法二比较灵活。
要想让自定义组件执行原生DOM点击事件,需要加事件修饰符
.native
,如:<Student ref="student" @click.native="show"/>
。vue在解析时发现@click.native="show"
,知道是原生dom事件,于是他把这个click事件交给了Student组件最外层的元素,如果不加事件修饰符.native
的话,他会认为@click
是自定义事件
解绑事件
如何解绑:你给谁绑定的事件,你就找谁去解绑事件。
解绑一个自定义事件:this.$off('事件名称')
解绑多个自定义事件:this.$off(['事件名称1','事件名称2',...])
解绑所有的自定义事件:this.$off()
Student.vue代码:
<template>
<div class="student">
<h2>学生姓名:{{name}}</h2>
<h2>学生性别:{{sex}}</h2>
<h2>当前求和为:{{number}}</h2>
<button @click="add">点我number++</button>
<button @click="sendStudentlName">把学生名给App</button>
<button @click="unbind">解绑atguigu事件</button>
<button @click="death">销毁当前Student组件的实例(vc)</button>
</div>
</template>
<script>
export default {
name:'Student',
data() {
return {
name:'张三',
sex:'男',
number:0
}
},
methods: {
add(){
console.log('add回调被调用了')
this.number++
},
sendStudentlName(){
//触发Student组件实例身上的atguigu事件:this.$emit('想触发的事件名',后面的参数可选且都是数据)
this.$emit('atguigu',this.name,666,888,900)
},
unbind(){
this.$off('atguigu') //解绑一个自定义事件:this.$off('事件名称')
// this.$off(['atguigu','demo']) //解绑多个自定义事件:this.$off(['事件名称1','事件名称2',...])
// this.$off() //解绑所有的自定义事件:this.$off()
},
death(){
this.$destroy() //销毁了当前Student组件的实例,销毁后所有Student实例的自定义事件全都不奏效,
// 但是原生的DOM事件(点击事件click)还能用。
}
},
}
</script>
注意:如果销毁了当前Student组件的实例,销毁后所有Student实例的自定义事件(unbind、sendStudentlName)全都不奏效,但是原生的DOM事件(点击事件click)还能用,即点击【点我number++】按钮,还能调用add()触发number++
同理,如果VM被销毁了,他所有子组件都没了,所有子组件中的自定义事件也都被销毁了,但是原生的DOM事件(点击事件click)还能用。
全局事件总线
一、全局事件总线是一种组件间通信的方式,适用于任意组件间通信。
二、安装全局事件总线:在main.js中创建vue实例时,将当前vue实例绑定到Vue.prototype.$bus
上
new Vue({
......
beforeCreate() {
Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm
},
......
})
三、使用事件总线:
- 接收数据:A组件想接收数据,则在A组件中给
$bus
绑定自定义事件,事件的回调留在A组件自身。
methods(){
demo(data){......}
}
......
mounted() {
this.$bus.$on('xxxx',this.demo)
}
- 提供数据:
this.$bus.$emit('xxxx',数据)
四、最好在beforeDestroy钩子中,用$off
去解绑当前组件所用到的事件。
需求:点击Student组件的按钮,把Student组件的name传给School组件。
- 首先在创建vue实例是安装全局事件总线,main.js中的代码:
import Vue from 'vue'//引入Vue
import App from './App.vue'//引入App
Vue.config.productionTip = false//关闭Vue的生产提示
new Vue({//创建vm
el:'#app',
render: h => h(App),
beforeCreate() {
Vue.prototype.$bus = this //安装全局事件总线
},
})
- 然后在Student组件中发送数据,Student.vue中的代码:
<template>
<div class="student">
<h2>学生姓名:{{name}}</h2>
<h2>学生性别:{{sex}}</h2>
<button @click="sendStudentName">把学生名给School组件</button>
</div>
</template>
<script>
export default {
name:'Student',
data() {
return {
name:'张三',
sex:'男',
}
},
methods: {
sendStudentName(){
this.$bus.$emit('hello',this.name)
}
},
}
</script>
- 最后在School组件中接收数据,并在School组件销毁前解绑事件。School.vue中的代码:
<template>
<div class="school">
<h2>学校名称:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
</div>
</template>
<script>
export default {
name:'School',
data() {
return {
name:'尚硅谷',
address:'北京',
}
},
mounted() {
this.$bus.$on('hello',(data)=>{
console.log('我是School组件,收到了数据',data)
})
},
beforeDestroy() {//组件销毁前解绑事件
this.$bus.$off('hello')
},
}
</script>
消息订阅与发布
一、消息订阅与发布是一种组件间通信的方式,适用于任意组件间通信。
二、思想:A组件想用C组件的消息,A组件是消息使用者,所以他订阅消息,C组件是消息提供者,所以他发布消息。A组件订阅名为demo的消息,并指定一个回调test。C组件会发布demo消息,发布时带上数据,此时A组件就会收到demo消息并调用test,数据作为参数传进了test函数中。
三、使用步骤:原生JS无法轻松实现消息订阅与发布,所以我们需要借助第三方库pubsub-js(你也可以用别的库,消息订阅与发布只是一个理念,有很多库把这个理念落到了实处),pubsub-js可以在任何框架中,轻松实现消息订阅与发布。
- 在项目终端安装pubsub-js:
npm i pubsub-js
- 引入:
import pubsub from 'pubsub-js'
四、接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身。
methods(){
demo(data){......}
}
......
mounted() {
this.pid = pubsub.subscribe('xxx',this.demo) //订阅消息
}
五、提供数据:pubsub.publish('xxx',数据)
六、最好在beforeDestroy钩子中,用PubSub.unsubscribe(pid)
去取消订阅。
七、需求:点击Student组件的按钮,把Student组件的name传给School组件。
八、思路:School组件接收数据,Student组件发送数据,双方都要用到pubsub-js,在两个组件中引入pubsub-js,引入的pubsub-js是个对象,身上有很多有用的方法。
- Student组件发布消息,Student组件中的代码:
<template>
<div class="student">
<h2>学生姓名:{{name}}</h2>
<h2>学生性别:{{sex}}</h2>
<button @click="sendStudentName">把学生名给School组件</button>
</div>
</template>
<script>
import pubsub from 'pubsub-js'
export default {
name:'Student',
data() {
return {
name:'张三',
sex:'男',
}
},
methods: {
sendStudentName(){
//发布消息:pubsub.publish('消息名',数据)
pubsub.publish('hello',666)
}
},
}
</script>
- School组件接收消息,School组件代码如下:
<template>
<div class="school">
<h2>学校名称:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
</div>
</template>
<script>
import pubsub from 'pubsub-js'// 引入pubsub-js,pubsub是个对象,身上有很多有用的方法
export default {
name:'School',
data() {
return {
name:'尚硅谷',
address:'北京',
}
},
// methods: {
// pub(msgName, data) {
// console.log(this)// this指向本组件实例,即School组件
// }
// },
mounted() {
/*
订阅消息:pubsub.subscribe('消息名',回调函数)
回调函数接收两个参数,分别是消息名和数据(规定的)
每次订阅消息都会获得一个订阅ID,有点像定时器
this.pubId = pubsub.subscribe('hello',function(msgName,data){
console.log(this)// this指向undefined,因为你这里使用的是第三方库
})
也可以将回调函数抽离成方法放在methods中,订阅时传递方法名即可,此时回调函数的this依旧指向本组件实例,即School组件
this.pubId = pubsub.subscribe('hello',this.pub)
*/
this.pubId = pubsub.subscribe('hello',(msgName,data)=>{
console.log(this)// this指向本组件实例,即School组件
console.log('有人发布了hello消息,hello消息的回调执行了',msgName,data)
})
},
beforeDestroy() {
//通过ID取消订阅:pubsub.unsubscribe(ID)
pubsub.unsubscribe(this.pubId)
},
}
</script>