Vue2:组件高级(上)
Date: May 20, 2023
Sum: 组件样式冲突、data函数、组件通信、props、组件生命周期、vue3.x中全局配置axios
目标:
能够掌握 watch 侦听器的基本使用
能够知道 vue 中常用的生命周期函数
能够知道如何实现组件之间的数据共享
能够知道如何在 vue3.x 的项目中全局配置 axios
前言:以下使用较老的axios,否则会报错
npm i axios@0.21.1 -S
组件之间的样式冲突
样式冲突问题:
默认情况下,写在 .vue 组件中的样式会全局生效,因此很容易造成多个组件之间的样式冲突问题
导致组件之间样式冲突的根本原因是:
① 单页面应用程序中,所有组件的 DOM 结构,都是基于唯一的 index.html 页面进行呈现的
② 每个组件中的样式,都会影响整个 index.html 页面中的 DOM 元素
思考:如何解决组件样式冲突的问题
为每个组件分配唯一的自定义属性,在编写组件样式时,通过属性选择器来控制样式的作用域,示例代码如下:
style 节点的 scoped 属性
为了提高开发效率和开发体验,vue 为 style 节点提供了 scoped 属性,从而防止组件之间的样式冲突问题:
注意:父组件与子组件都要的style都要加上scoped
默认情况:写在组件中的样式会 全局生效 一因此很容易造成多个组件之问的样式冲突问题
- 全局样式:默认组件中的样式会作用到全局
- 局部样式:可以给组件加上 scoped 属性,可以让样式只作用于当前组件
原理:
- 当前组件内标签都被添加data-v-hash值 的属性
- css选择器都被添加 [data-v-hash值] 的属性选择器
最终效果: 必须是当前组件的元素, 才会有这个自定义属性, 才会被这个样式作用到
/deep/ 样式穿透
如果给当前组件的 style 节点添加了 scoped 属性,则当前组件的样式对其子组件是不生效的。如果想让某些样式对子组件生效,可以使用 /deep/ 深度选择器。
注意:/deep/ 是 vue2.x 中实现样式穿透的方案。在 vue3.x 中推荐使用 :deep() 替代 /deep/。
Vue3中的样式结构::deep(标签)
<style lang="less" scoped>
p {
color: red;
}
:deep(h3) {
color: blue
}
</style>
data必须是一个函数
原因:
目的:保证每个组件实例,维护独立的一份数据对象。
每次创建新的组件实例,都会新执行一次data 函数,得到一个新对象。
举例:
data()函数能够保证每个组件的数据是独立的
组件通信
基础概念:
基础概念:
组件通信:指组件与组件之间的数据传递
- 组件的数据是独立的,无法直接访问其他组件的数据。
- 想使用其他组件的数据,就需要组件通信
组件之间的关系:
在项目开发中,组件之间的关系分为如下 3 种:
① 父子关系 ② 兄弟关系 ③ 后代关系
AB是父子关系,BC有一个共同的父级节点,故二者为兄弟关系。B和EFI都为特殊的兄弟关系。
A和DGH属于后代关系
父子组件之间的数据共享
父子组件之间的数据共享又分为:
① 父 -> 子共享数据 ② 子 -> 父共享数据 ③ 父 <-> 子双向数据同步
通信过程:
1-父组件通过 props 将数据传递给子组件
2-子组件利用 $emit 通知父组件修改更新
父向子组件共享数据
父组件通过 v-bind 属性绑定向子组件共享数据。同时,子组件需要使用 props 接收数据。
案例:
App.vue
<template>
<div>
<h1>MyAPP -- {{ count }}</h1>
<!-- 1.给最爱你标签,添加属性的方式,传值 -->
<button @click="count += 1">父+1</button>
<my-son :num="count"></my-son>
</div>
</template>
<script>
import MySon from './Son.vue'
export default {
name: 'MyApp',
components: {
MySon,
},
data() {
return {
count: 0,
}
}
}
</script>
MySon.vue
<template>
<div>
<!-- 3.渲染使用 -->
<h2>MySon -- {{ num }}</h2>
</div>
</template>
<script>
export default {
name: 'MySon',
// 2. 通过props进行接收
props: ['num']
}
</script>
效果:
子向父组件共享数据
子组件通过自定义事件的方式向父组件共享数据。
具体步骤:
子组件:
- 声明自定义事件 2. 数据变化时,触发自定义事件
父组件:
- 监听子组件的自定义事件 numchang 2.通过形参,接收子组件传递过来的数据
案例:
App.vue
<template>
<div>
<h1>MyAPP -- {{ count }}</h1>
<button @click="count += 1">父+1</button>
<!-- 1. 监听子组件的自定义事件 numchange -->
<my-son :num="count" @numchange="getNum"></my-son>
</div>
</template>
<script>
import MySon from './Son.vue'
export default {
name: 'MyApp',
components: {
MySon,
},
data() {
return {
count: 0,
}
},
methods: {
getNum(num) { // 2. 通过形参,接收子组件传递过来的数据
this.count = num
}
}
}
</script>
Son.vue
<template>
<div>
<h2>MySon -- {{ num }}</h2>
<button @click="add">+1</button>
</div>
</template>
<script>
export default {
name: 'MySon',
props: ['num'],
emits: ['numchange'], //1. 声明自定义事件
methods: {
add() {
this.$emit('numchange', this.num + 1) //2,数据变化时,触发自定义事件
}
}
}
</script>
效果:
父子组件之间数据的双向同步
父组件在使用子组件期间,可以使用 v-model 指令维护组件内外数据的双向同步:
具体步骤:
- 父组件向子组件的props中传递数据
- 这里通过 v-model 方式进行双向数据绑定,维护组件两方数据同步
- 子组件声明emits属性,组件内的元素需要以 update: 的方式开头,这里需要更新哪个数据,就把相应数据的值丢过来,比如number
- 通过 $emits 的方式将数据发送出去
好处:父组件中不用再监听自定义事件,也不用再额外定义事件处理函数
案例:
-
Code:
App.vue
<template> <div> <h1>MyAPP -- {{ count }}</h1> <button @click="count += 1">父+1</button> <my-son v-model:num="count" ></my-son> </div> </template> <script> import MySon from './Son.vue' export default { name: 'MyApp', components: { MySon, }, data() { return { count: 0, } }, } </script>
Son.vue
<template> <div> <h2>MySon -- {{ num }}</h2> <button @click="add">+1</button> </div> </template> <script> export default { name: 'MySon', props: ['num'], emits: ['update:num'], methods: { add() { // this.$emit('numchange', this.num + 1) this.$emit('update:num', this.num + 1) } } } </script>
效果:
兄弟组件之间的数据共享
2023Vue教程的做法
**作用:**非父子组件之间,进行简易消息传递。(复杂场景→ Vuex)
步骤:
1-创建一个都能访问的事件总线 (空Vue实例)
注:把这个放在utils下的EventBus.js中
import Vue from 'vue'
const Bus = new Vue()
export default Bus
2-A组件(接受方),监听Bus的 $on事件
// 先导入Bus
import Bus from '../utils/EventBus'
export default {
data() {
return {
msg: '',
}
},
// 再从 created 阶段就监听 $on 事件
created() {
Bus.$on('sendMsg', (msg) => {
// console.log(msg)
this.msg = msg
})
},
}
3-B组件(发送方),触发Bus的$emit事件
注:这个在组件内
import Bus from '../utils/EventBus'
export default {
methods: {
sendMsgFn() {
Bus.$emit('sendMsg', '今天天气不错,适合旅游')
},
},
}
图示:
注意:这是个一对多的发送
案例:传递A组件数据给B组件
-
Code:
BaseA.vue
<template> <div class="base-a"> 我是A组件(接受方) <p>{{msg}}</p> </div> </template> <script> import Bus from '../utils/EventBus' export default { data() { return { msg: '', } }, created() { Bus.$on('sendMsg', (msg) => { // console.log(msg) this.msg = msg }) }, } </script> <style scoped> .base-a { width: 200px; height: 200px; border: 3px solid #000; border-radius: 3px; margin: 10px; } </style>
BaseB.vue
<template> <div class="base-b"> <div>我是B组件(发布方)</div> <button @click="sendMsgFn">发送消息</button> </div> </template> <script> import Bus from '../utils/EventBus' export default { methods: { sendMsgFn() { Bus.$emit('sendMsg', '今天天气不错,适合旅游') }, }, } </script> <style scoped> .base-b { width: 200px; height: 200px; border: 3px solid #000; border-radius: 3px; margin: 10px; } </style>
2021Vue教程的做法:
兄弟组件之间实现数据共享的方案是 EventBus。
可以借助于第三方的包 mitt 来创建 eventBus 对象,从而实现兄弟组件之间的数据共享。
示意图如下:
理解:在数据接收方调用on方法来声明自定义事件,在数据发送方通过emit方法来触发emit事件
3.1 安装 mitt 依赖包
在项目中运行如下的命令,安装 mitt 依赖包:
npm install mitt@2.1.0
3.2 创建公共的 EventBus 模块
在项目中创建公共的 eventBus 模块如下:
// eventBus.js
// 导入 mitt 包
import mitt from 'mitt'
// 创建 EventBus 的实例对象
const bus = mitt()
// 将 EventBus 的实例对象共享出去
export default bus
3.3 在数据接收方自定义事件
在数据接收方,调用 bus.on(‘事件名称’, 事件处理函数) 方法注册一个自定义事件。
示例代码如下:
// 导入 eventBus.js 模块, 得到共享的bus对象
export default {
data() {return { count: 0}},
created() {
// 在created生命周期函数中声明自定义事件
// 调用 bus.on 方法注册一个自定义事件,通过事件处理函数的形参数接收数据
bus.on('countChange', (count) => {
this.count = count
})
}
}
3.4 在数据接发送方触发事件
在数据发送方,调用 bus.emit(‘事件名称’, 要发送的数据) 方法触发自定义事件。示例代码如下:
// 导入 eventBus.js 模块,得到共享的 bus 对象
import bus from './eventBus.js'
export default {
data() {return { count: 0}},
methods: {
addCount() {
this.count++
bus.emit('countChange', this.count) // 调用 bus.emit() 方法触发自定义事件,并发送数据
}
}
}
案例:
-
Code:
Left.vue
<template> <div> <h2>Left--数据发送方--num的值为: {{ count }}</h2> <button @click="addCount">+1</button> </div> </template> <script> import bus from './eventBus.js' export default { name: 'MyLeft', data() { return { count: 0, } }, methods: { addCount() { this.count++ bus.emit('countChange', this.count) } } } </script>
Right.vue
<template> <div> <h2>Right--数据接收方--num的值为:{{ num }}</h2> </div> </template> <script> import bus from './eventBus.js' export default { name: 'MyRight', data() { return { num: 0, } }, created() { bus.on('countChange', count => { this.num = count }) } } </script>
效果:
后代关系组件之间的数据共享-provide&inject
作用:跨层级共享数据
场景:
后代关系组件之间共享数据,指的是父节点的组件向其子孙组件共享数据。此时组件之间的嵌套关系比较复杂,可以使用 provide 和 inject 实现后代关系组件之间的数据共享。
语法:
1-父组件 provide提供数据
export default {
provide () {
return {
// 普通类型【非响应式】
color: this.color,
// 复杂类型【响应式】
userInfo: this.userInfo,
}
}
}
2-子/孙组件 inject 获取数据
export default {
inject: ['color','userInfo'],
created () {
console.log(this.color, this.userInfo)
}
}
图示:
注意:
1-provide提供的简单类型的数据不是响应式的,复杂类型数据是响应式。(推荐提供复杂类型数据)如上图所示,如果我用button修改color,那么图中元素不会有变动,而用button修改userInfo中的数据,则图中相应元素会有变动。
2-子/孙组件通过inject获取的数据,不能在自身组件内修改
补充:2021版的Vue课程
父节点对外共享响应式的数据
值得注意的是,provide中return回去的数据,并非是响应式的数据,即若我在父组件中用button修改p标签的颜色,子组件的中的p标签颜色不会跟着一块变。
父节点使用 provide 向下共享数据时,可以结合 computed 函数向下共享响应式的数据。示例代码如下:
子孙节点使用响应式的数据
如果父级节点共享的是响应式的数据,则子孙节点必须以 .value 的形式进行使用。示例代码如下:
vuex
vuex 是终极的组件之间的数据共享方案。在企业级的 vue 项目开发中,vuex 可以让组件之间的数据共享变得高效、清晰、且易于维护。
个人总结:
父子关系
① 父 -> 子 属性绑定
② 子 -> 父 事件绑定
③ 父 <-> 子 组件上的 v-model
兄弟关系
④ EventBus
后代关系
⑤ provide & inject
全局数据共享
⑥ vuex
组件的 props
为了提高组件的复用性,在封装 vue 组件时需要遵守如下的原则:
组件的 DOM 结构、Style 样式 要尽量复用
组件中要展示的数据,尽量由组件的使用者提供
为了方便使用者为组件提供要展示的数据,vue 组件提供了 props 的概念。
基础概念:
概念:组件上 注册的一些 自定义属性
作用:父组件通过 props 向子组件传递要展示的数据
特点:可以传递 任意数量与类型 的prop ;提高了组件的复用性
语法:简易写法
子组件接收
props: ['数据1', '数据2']
举例:
传递父组件中的数据到子组件中
使用 v-bind 属性绑定的形式,为组件动态绑定 props 的值
注意: :username=”username”
左边是子,右边是父
效果:
-
Code:
App.vue
<template> <div class="app"> <UserInfo :username="username" :age="age" :isSingle="isSingle" :car="car" :hobby="hobby" ></UserInfo> </div> </template> <script> import UserInfo from './components/UserInfo.vue' export default { data() { return { username: '小帅', age: 28, isSingle: true, car: { brand: '宝马', }, hobby: ['篮球', '足球', '羽毛球'], } }, components: { UserInfo, }, } </script> <style> </style>
UserInfo.vue
<template> <div class="userinfo"> <h3>我是个人信息组件</h3> <div>姓名:{{ username }}</div> <div>年龄:{{ age }}</div> <div>是否单身:{{ isSingle }}</div> <div>座驾:{{ car.brand }}</div> <div>兴趣爱好 {{ hobby.join('、') }}</div> </div> </template> <script> export default { props: ['username', 'age', 'isSingle', 'car', 'hobby'] } </script> <style> .userinfo { width: 300px; border: 3px solid #000; padding: 20px; } .userinfo > div { margin: 20px 10px; } </style>
props校验
作用:为组件的 prop 指定验证要求,不符合要求,控制台就会有错误提示 → 帮助开发者,快速发现错误
语法:
- 类型校验(最常用)
props: {
校验的属性名:类型 // Number String Boolean ...
}
- 非空校验
- 默认值
- 自定义校验
举例:进度条的进度只能传入数字而不能是其他的数据类型
BaseProgress.vue 子组件接收父组件的数据
export default {
// 1.基础写法(类型校验)
props: {
w: Number,
},
}
props校验完整写法
类型校验是最常用的,如果你需要后面几种校验,就需要补充以下的写法:
语法:
props: {
校验的属性名: {
type: 类型, // Number String Boolean ...
required: true, // 是否必填
default: 默认值, // 默认值
validator (value) {
// 自定义校验逻辑
return 是否通过校验
}
}
},
代码示例:
<script>
export default {
// 完整写法(类型、默认值、非空、自定义校验)
props: {
w: {
type: Number,
//required: true,
default: 0,
validator(val) {
// console.log(val)
if (val >= 100 || val <= 0) {
console.error('传入的范围必须是0-100之间')
return false
} else {
return true
}
},
},
},
}
</script>
注意:
1.default和required一般不同时写(因为当时必填项时,肯定是有值的)
2.default后面如果是简单类型的值,可以直接写默认。如果是复杂类型的值,则需要以函数的形式return一个默认值
props&data、单向数据流
**共同点:**都可以给组件提供数据
区别:
- data 的数据是自己的 → 随便改
- prop 的数据是外部的 → 不能直接改,要遵循 单向数据流
单向数据流:
父级props 的数据更新,会向下流动,影响子组件。这个数据流动是单向的,即父的数据更新流向子
子若想影响父的数据,需要通过$.emit来影响父。然后,父在将数据单向流动给子。
口诀:谁的数据谁负责
案例:子接收父Count值,并且子通过 this.$emit 传递方法给父,让其修改数据
子想要改变父数据,需要通过 this.$emit 进行传递数据
父接收子changeCount方法,并利用 handleChange 接收数据,从而修改自身Count值
注意:@changeCount=”handleChange”
左边是子,右边是父
-
Code:
App.vue
<template> <div class="app"> <BaseCount @changeCount="handleChange" :count="count" ></BaseCount> </div> </template> <script> import BaseCount from './components/BaseCount.vue' export default { components:{ BaseCount }, data(){ return { count:100 } }, methods:{ handleChange(newCount) { this.count = newCount } } } </script> <style> </style>
BaseCount.vue
<template> <div class="base-count"> <button @click="handleSub()">-</button> <span>{{ count }}</span> <button @click="handleAdd()">+</button> </div> </template> <script> export default { // 1.自己的数据随便修改 (谁的数据 谁负责) // data () { // return { // count: 100, // } // }, // 2.外部传过来的数据 不能随便修改 props: { count: Number, }, methods: { handleAdd() { this.$emit('changeCount', this.count + 1) }, handleSub() { this.$emit('changeCount', this.count - 1) } } } </script> <style> .base-count { margin: 20px; } </style>
props 的大小写命名
组件中如果使用“camelCase (驼峰命名法)”声明了 props 属性的名称,
则有两种方式为其绑定属性的值:
理解:
封装的时候采用驼峰命名法,那么外界在传递属性的时候既可以通过短横线命名,也可以通过驼峰命名法命名
注意:
如果我们在组件命名属性时采用驼峰命名法,
<script>
export default {
name: 'MyArticle',
// 外界可以传递指定的数据,到当前的组件中
props: ['author', 'title', 'MyTest']
}
</script>
那么,在传递属性时,我们既可以使用驼峰命名法,也可以使用短横线命名法
<my-article :title="info.title" :author="info.author" :MyTest="info.MyTest"></my-article>
案例:小黑记事本-组件版
案例效果:
需求说明:
- 拆分基础组件
- 渲染待办任务
- 添加任务
- 删除任务
- 底部合计 和 清空功能
- 持久化存储
拆分基础组件:
咱们可以把小黑记事本原有的结构拆成三部分内容:头部(TodoHeader)、列表(TodoMain)、底部(TodoFooter)
思路:
具体操作:
1-拆分并渲染
/**
* 渲染功能:
* 1. 子组件提供数据给父组件
* 2. 父传数据给子
* 3. 利用 v-for 渲染数据
*/
-
Code:
App.vue
<template> <!-- 主体区域 --> <section id="app"> <TodoHeader></TodoHeader> <TodoMain :list="list"></TodoMain> <TodoFooter></TodoFooter> </section> </template> <script> import TodoHeader from './components/TodoHeader.vue' import TodoMain from './components/TodoMain.vue' import TodoFooter from './components/TodoFooter.vue' /** * 渲染功能: * 1. 子组件提供数据给父组件 * 2. 父传数据给子 * 3. 利用 v-for 渲染数据 */ export default { components: { TodoHeader, TodoMain, TodoFooter, }, data () { return { list: [ { id: 1, name: '打篮球1'}, { id: 2, name: '打篮球1'}, { id: 3, name: '打篮球1'} ] } } } </script> <style> </style>
TodoMain.vue
<template> <div> <!-- 列表区域 --> <section class="main"> <ul class="todo-list"> <li class="todo" v-for="(item, index) in list" :key="item.id"> <div class="view"> <span class="index">{{ index + 1 }}.</span> <label>{{ item.name }}</label> <button class="destroy" ></button> </div> </li> </ul> </section> </div> </template> <script> export default { name: 'TodoMain', data() { return { } }, props: { list: Array }, methods: { }, } </script> <style> </style>
2-添加功能(添加、删除、统计、清空、持久化存储)
/**
* 添加功能:
* 1. 收集表单数据 v-model
* 2. 监听事件(回车+点击 都要进行添加)
* 3. 子传父,将任务名称传递给父组件
* 4. 父组件进行添加 unshift(自己的数据自己负责)
*/
/**
* 删除功能:
* 1. 监听时间(监听删除的点击)携带id
* 2. 子传父,将删除的id传递给父组件App.vue
* 3. 进行删除 filter (自己的数据自己负责)
*/
// 底部合计:父组件传递list到底部组件 —>展示合计
// 清空功能:监听事件 —> **子组件**通知父组件 —>父组件清空
// 持久化存储: watch监听数据变化,持久化到本地
-
Code:
App.vue
<template> <!-- 主体区域 --> <section id="app"> <TodoHeader @add="handleAdd"></TodoHeader> <TodoMain :list="list" @del="handleDel"></TodoMain> <TodoFooter :total="this.list.length" @clear="handleClear"></TodoFooter> </section> </template> <script> import TodoHeader from './components/TodoHeader.vue' import TodoMain from './components/TodoMain.vue' import TodoFooter from './components/TodoFooter.vue' /** * 渲染功能: * 1. 子组件提供数据给父组件 * 2. 父传数据给子 * 3. 利用 v-for 渲染数据 */ /** * 添加功能: * 1. 收集表单数据 v-model * 2. 监听事件(回车+点击 都要进行添加) * 3. 子传父,将任务名称传递给父组件 * 4. 父组件进行添加 unshift(自己的数据自己负责) */ /** * 删除功能: * 1. 监听时间(监听删除的点击)携带id * 2. 子传父,将删除的id传递给父组件App.vue * 3. 进行删除 filter (自己的数据自己负责) */ /** * 持久化存储: watch监听数据变化,持久化到本地 */ const defaultList = [ { id: 1, name: '打篮球1'}, { id: 2, name: '打篮球1'}, { id: 3, name: '打篮球1'} ] export default { components: { TodoHeader, TodoMain, TodoFooter, }, data () { return { list: JSON.parse(localStorage.getItem('list')) || defaultList, } }, methods: { handleAdd(todoName) { this.list.unshift({ id: +new Date(), name: todoName, }) }, handleDel(id) { // console.log(id); this.list = this.list.filter(item => item.id !== id) }, handleClear() { this.list = [] } }, watch: { list: { deep: true, handler(newValue) { localStorage.setItem('list', JSON.stringify(newValue)) } } } } </script> <style> </style>
TodoHeader.vue
<template> <div> <!-- 输入框 --> <header class="header"> <h1>小黑记事本</h1> <input placeholder="请输入任务" class="new-todo" v-model="todoName" @keyup.enter="handleAdd"/> <button class="add" @click="handleAdd" >添加任务</button> </header> </div> </template> <script> export default { name: 'TodoHeader', data() { return { todoName: '', } }, methods: { handleAdd() { if(this.todoName.trim() === '') { alert("请输入内容!") return } this.$emit('add', this.todoName) this.todoName = '' }, }, } </script> <style> </style>
TodoMain.vue
<template> <div> <!-- 列表区域 --> <section class="main"> <ul class="todo-list"> <li class="todo" v-for="(item, index) in list" :key="item.id"> <div class="view"> <span class="index">{{ index + 1 }}.</span> <label>{{ item.name }}</label> <button class="destroy" @click="handleDel(item.id)"></button> </div> </li> </ul> </section> </div> </template> <script> export default { name: 'TodoMain', data() { return { } }, props: { list: Array }, methods: { handleDel(id) { // console.log(id); this.$emit("del", id) } }, } </script> <style> </style>
TodoFooter.vue
<template> <div> <!-- 统计和清空 --> <footer class="footer"> <!-- 统计 --> <span class="todo-count">合 计:<strong> {{ total }} </strong></span> <!-- 清空 --> <button class="clear-completed" @click="clear"> 清空任务 </button> </footer> </div> </template> <script> export default { name: 'TodoFooter', data() { return { } }, props: { total: Number, }, methods: { clear() { this.$emit('clear') } }, } </script> <style> </style>
v-model 原理
基本原理
**原理:**v-model本质上是一个语法糖。例如应用在输入框上,就是value属性 和 input事件 的合写
结合这段代码理解:
<template>
<div id="app" >
<input v-model="msg" type="text">
<input :value="msg" @input="msg = $event.target.value" type="text">
</div>
</template>
注意:$event 用于在模板中,获取事件的形参
作用:v-model提供数据的双向绑定
- 数据变,视图跟着变 :value
- 视图变,数据跟着变 @input
代码实例:两个input是同步进退的
<template>
<div class="app">
<input type="text" v-model="msg1"/>
<br />
<input type="text" :value="msg1" @input="msg=$event.target.value">
</div>
</template>
<script>
export default {
data() {
return {
msg1: '',
}
},
}
</script>
<style>
</style>
v-model使用在其他表单元素上的原理
不同的表单元素, v-model在底层的处理机制是不一样的。比如给checkbox使用v-model底层处理的是 checked属性和change事件。
不过咱们只需要掌握应用在文本框上的原理即可
表单类组件封装
目标:实现子组件和父组件数据的双向绑定
案例:实现App.vue中的selectId和子组件选中的数据进行双向绑定
App.vue
<template>
<div class="app">
<BaseSelect
:cityId = "selectId"
@changeId="selectId = $event" //用 $event 表示当前形参
></BaseSelect>
</div>
</template>
<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
data() {
return {
selectId: '104',
}
},
components: {
BaseSelect,
},
}
</script>
<style>
</style>
BaseSelect.vue
<template>
<div>
<select :value="cityId" @change="handleChange">
<option value="101">北京</option>
<option value="102">上海</option>
<option value="103">武汉</option>
<option value="104">广州</option>
<option value="105">深圳</option>
</select>
</div>
</template>
<script>
export default {
props: {
cityId: String,
},
methods: {
// e 指触发事件的事件源
handleChange(e) {
console.log(e.target.value);
this.$emit('changeId', e.target.value)
}
}
}
</script>
<style>
</style>
v-model 简化代码
**目标:**父组件通过v-model 简化代码,实现子组件和父组件数据 双向绑定
简化:
v-model其实就是 :value和@input事件的简写
- 子组件:props通过value接收数据,事件触发 input
- 父组件:v-model直接绑定数据
案例:
子组件
<select :value="value" @change="handleChange">...</select>
props: {
value: String
},
methods: {
handleChange (e) {
// 将这里的handleChange改成input
this.$emit('input', e.target.value)
}
}
父组件
<BaseSelect v-model="selectId"></BaseSelect>
总结:
可以结合上面的 父子组件之间数据 的双向同步
.sync修饰符(建议对比理解前几个双向绑定)
作用:可以实现 子组件 与 父组件数据 的 双向绑定,简化代码
简单理解:子组件可以修改父组件传过来的props值
特点:prop属性名,可以自定义,非固定为 value (这与v-model不同)
场景: 封装弹框类的基础组件, visible属性 true显示 false隐藏
理解:如果封装的不是value,而是这种弹框类的组件,建议用.sync建立双向绑定
本质: .sync修饰符 就是 :属性名 和 @update:属性名 合写
语法
父组件
//.sync写法
<BaseDialog :visible.sync="isShow" />
--------------------------------------
//完整写法
<BaseDialog
:visible="isShow"
@update:visible="isShow = $event"
/>
子组件
props: {
visible: Boolean
},
this.$emit('update:visible', false)
案例:
-
Code:
App.vue
<template> <div class="app"> <button @click="isShow = true" >退出按钮</button> <BaseDialog :visible.sync="isShow" ></BaseDialog> </div> </template> <script> import BaseDialog from "./components/BaseDialog.vue" export default { data() { return { isShow: false } }, methods: { }, components: { BaseDialog, }, } </script> <style> </style>
BaseDialog.vue
<template> <div class="base-dialog-wrap" v-show="visible"> <div class="base-dialog"> <div class="title"> <h3>温馨提示:</h3> <button class="close" @click="close">x</button> </div> <div class="content"> <p>你确认要退出本系统么?</p> </div> <div class="footer"> <button>确认</button> <button>取消</button> </div> </div> </div> </template> <script> export default { props: { visible: Boolean, }, methods: { close() { this.$emit('update:visible', false) } } } </script> <style scoped> .base-dialog-wrap { width: 300px; height: 200px; box-shadow: 2px 2px 2px 2px #ccc; position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); padding: 0 10px; } .base-dialog .title { display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #000; } .base-dialog .content { margin-top: 38px; } .base-dialog .title .close { width: 20px; height: 20px; cursor: pointer; line-height: 10px; } .footer { display: flex; justify-content: flex-end; margin-top: 26px; } .footer button { width: 80px; height: 40px; } .footer button:nth-child(1) { margin-right: 10px; cursor: pointer; } </style>
ref和$refs
作用: 利用ref 和 $refs 可以用于 获取 dom 元素 或 组件实例
理解:每个 vue 的组件实例上,都包含一个 $refs 对象,里面存储着对应的 DOM 元素或组件的引用。默认情况下,组件的 $refs 指向一个空对象。
**特点:**查找范围 → 当前组件内(更精确稳定)
技术诞生原因:
如果使用querySelector进行查找图标.box, 可能在整个页面找到多个.box。
因此,为了更加精准地获取DOM元素,就需要ref与$refs这种技术
语法
1.给要获取的盒子添加ref属性
<div ref="chartRef">我是渲染图表的容器</div>
2.获取时通过 r e f s 获取 t h i s . refs获取 this. refs获取this.refs.chartRef 获取
mounted () {
console.log(this.$refs.chartRef)
}
注意
之前只用document.querySelect(‘.box’) 获取的是整个页面中的盒子
案例-1:获取DOM元素
-
Code:
App.vue
<template> <div class="app"> <div class="base-chart-box"> 这是一个捣乱的盒子 </div> <BaseChart></BaseChart> </div> </template> <script> import BaseChart from './components/BaseChart.vue' export default { components:{ BaseChart } } </script> <style> .base-chart-box { width: 200px; height: 100px; } </style>
BaseChart.vue
<template> <div ref="mychart" class="base-chart-box">子组件</div> </template> <script> import * as echarts from 'echarts' export default { mounted() { // 基于准备好的dom,初始化echarts实例 const myChart = echarts.init(this.$refs.mychart) // 绘制图表 myChart.setOption({ title: { text: 'ECharts 入门示例', }, tooltip: {}, xAxis: { data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'], }, yAxis: {}, series: [ { name: '销量', type: 'bar', data: [5, 20, 36, 10, 10, 20], }, ], }) }, } </script> <style scoped> .base-chart-box { width: 400px; height: 300px; border: 3px solid #000; border-radius: 6px; } </style>
案例-2:获取组件实例
-
Code:
App.vue
<template> <div class="app"> <h4>父组件 -- <button>获取组件实例</button></h4> <BaseForm ref="baseForm"></BaseForm> <button @click="handleGet">父-获取数据</button> <button @click="handleReset">父-重置数据</button> </div> </template> <script> import BaseForm from './components/BaseForm.vue' export default { components: { BaseForm, }, methods: { handleGet() { this.$refs.baseForm.getFormData() }, handleReset() { this.$refs.baseForm.resetFormData() } } } </script> <style> </style>
BaseForm.vue
<template> <div class="app"> <div> 账号: <input v-model="username" type="text"> </div> <div> 密码: <input v-model="password" type="text"> </div> <div> <button @click="getFormData">获取数据</button> <button @click="resetFormData">重置数据</button> </div> </div> </template> <script> export default { data() { return { username: 'admin', password: '123456', } }, methods: { getFormData() { console.log('获取表单数据', this.username, this.password); }, resetFormData() { this.username = '' this.password = '' console.log('重置表单数据成功'); }, } } </script> <style scoped> .app { border: 2px solid #ccc; padding: 10px; } .app div{ margin: 10px 0; } .app div button{ margin-right: 8px; } </style>
异步更新 & $nextTick
需求:
编辑标题, 编辑框自动聚焦
- 点击编辑,显示编辑框
- 让编辑框,立刻获取焦点
**代码实现:
<template>
<div class="app">
<div v-if="isShowEdit">
<input type="text" v-model="editValue" ref="inp" />
<button>确认</button>
</div>
<div v-else>
<span>{{ title }}</span>
<button @click="editFn">编辑</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
title: '大标题',
isShowEdit: false,
editValue: '',
}
},
methods: {
editFn() {
// 显示输入框
this.isShowEdit = true
// 获取焦点
this.$refs.inp.focus()
} },
}
</script>
问题:
“显示之后”,立刻获取焦点是不能成功的!
原因:Vue 是异步更新DOM (提升性能)
解决方案
$nextTick:等 DOM更新后,才会触发执行此方法里的函数体
语法: this.$nextTick(函数体)
this.$nextTick(() => {
this.$refs.inp.focus()
})
注意:$nextTick 内的函数体 一定是箭头函数,这样才能让函数内部的this指向Vue实例
补充:用setTimeout也能实现,但是它的时间没有$nextTick精准
解决代码:
-
Code:
App.vue
<template> <div class="app"> <div v-if="isShowEdit"> <input type="text" v-model="editValue" ref="inp" /> <button>确认</button> </div> <div v-else> <span>{{ title }}</span> <button @click="handleEidt">编辑</button> </div> </div> </template> <script> export default { data() { return { title: '大标题', isShowEdit: false, editValue: '', } }, methods: { handleEidt() { //1. 显示输入框 this.isShowEdit = true //2. 让输入框获取焦点 this.$nextTick(() => { this.$refs.inp.focus() }) } }, } </script> <style> </style>
BaseForm.vue
<template> <div class="app"> <div> 账号: <input v-model="username" type="text"> </div> <div> 密码: <input v-model="password" type="text"> </div> <div> <button @click="getFormData">获取数据</button> <button @click="resetFormData">重置数据</button> </div> </div> </template> <script> export default { data() { return { username: 'admin', password: '123456', } }, methods: { getFormData() { console.log('获取表单数据', this.username, this.password); }, resetFormData() { this.username = '' this.password = '' console.log('重置表单数据成功'); }, } } </script> <style scoped> .app { border: 2px solid #ccc; padding: 10px; } .app div{ margin: 10px 0; } .app div button{ margin-right: 8px; } </style>
总结:
组件的生命周期
组件运行的过程:
组件的生命周期指的是:组件从创建 -> 运行(渲染) -> 销毁的整个过程,强调的是一个时间段
监听组件的不同时刻的方式
vue 框架为组件内置了不同时刻的生命周期函数,生命周期函数会伴随着组件的运行而自动调用。
例如:
① 当组件在内存中被创建完毕之后,会自动调用 created 函数
② 当组件被成功的渲染到页面上之后,会自动调用 mounted 函数
③ 当组件被销毁完毕之后,会自动调用 unmounted 函数
案例:
-
Code:
App.vue
<template> <div> <h1>App 根组件</h1> <hr/> <life-cycle v-if="flag"></life-cycle> <button @click="flag = !flag">Toggle</button> </div> </template> <script> import LifeCycle from './LifeCycle.vue' export default { name: 'MyApp', components: { LifeCycle }, data() { return { flag: false, } }, } </script> <style> </style>
LifeCycle.vue
<template> <div> <h2>LifeCycle</h2> </div> </template> <script> export default { name: 'LifeCycle', created() { console.log('组件在内存中被创建完毕了'); }, mounted() { console.log('组件被成功渲染到页面上了'); }, unmounted() { console.log('组件被销毁完毕了'); } } </script> <style> </style>
理解:代码中的created mounted() unmounted函数放到子组件LifeCycle中,当子组件创建完毕之后,会调用created函数,当组件被渲染到页面上后,会调用mounted函数,当组件被销毁完毕之后,会调用unmounted函数。
监听组件的更新的方式
当组件的 data 数据更新之后,vue 会自动重新渲染组件的 DOM 结构,从而保证 View 视图展示的数据和Model 数据源保持一致。
当组件被重新渲染完毕之后,会自动调用 updated 生命周期函数。
案例:
-
Code:
<template> <div> <h2>LifeCycle</h2> <p>{{ count }}</p> <button @click="count += 1">+1</button> </div> </template> export default { name: 'LifeCycle', data() { return { count: 0, } }, updated() { console.log('组件被重新渲染完毕了'); }, }
组件中主要的生命周期函数
注意:在实际开发中,created 是最常用的生命周期函数!
组件中全部的生命周期函数
疑问:为什么不在 beforeCreate 中发 ajax 请求初始数据
发起Ajax请求最好都在creat中
完整的生命周期图示
可以参考 vue 官方文档给出的“生命周期图示”,进一步理解组件生命周期执行的过程:
https://www.vue3js.cn/docs/zh/guide/instance.html#生命周期图示
vue 3.x 中全局配置 axios
- 为什么要全局配置 axios
在实际项目开发中,几乎每个组件中都会用到 axios 发起数据请求。此时会遇到如下两个问题:
① 每个组件中都需要导入 axios(代码臃肿)
② 每次发请求都需要填写完整的请求路径(不利于后期的维护)
2. 如何全局配置 axios
在 main.js 入口文件中,通过 app.config.globalProperties 全局挂载 axios,示例代码如下:
-
Code:
main.js
const app = createApp(MyApp) axios.defaults.baseURL = 'https://www.escook.cn' //为axios配置请求的根路径 //将axios挂载为app的全局自定义属性之后, //每个组件可以通过this直接访问到全局挂载的自定义属性 app.config.globalProperties.$http = axios
GetInfo:
export default { name: 'GetInfo', methods: { async getInfo() { const { data: res } = await this.$http.get('/api/get', { params: { name: 'ls', age: 33, }, }) console.log(res) }, }, }
PostInfo:
export default { name: 'PostInfo', methods: { async postInfo() { const { data: res } = await this.$http.post('/api/post', { name: 'zs', age: 20 }) console.log(res) }, }, }