Vue03-组件化

01. 组件化思想

当我们面对一个复杂问题的时候,常见的、高效的做法就是对复杂问题进行拆分,

将复杂问题拆分成一个个小的、简单的问题,

逐一解决小问题,再将处理好的小问题整合到一起,

如此解决复杂问题。

image-20230525000514877

在开发中,我们也是用同样的思路去完成一个大的项目:

  • 将一个整体功能代码拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,

  • 一个个功能块拆分后,就可以像搭建积木一下来搭建我们的项目。

这样做的好处除了降低开发的难度、提高代码复用率以外,也利于项目的管理和维护。

无论是前端三大框架(Vue、React、Angular),还是跨平台方案的Flutter,甚至是移动端都在转向组件化开发,包括小程序的开发也是采用组件化开发的思想。

02. 组件化基础

前面我们都是用Vue的createApp方法,传入一个对象来创建app的。

<script>
// 1.创建app
const app = Vue.createApp({
// data: option api
data() {
return {
message: "Hello Vue"
}
},
})
// 2.挂载app
app.mount("#app")
</script>

实际上,我们也可以先定义App对象,再将对象传入。

这个对象本质上就是一个组件,也就是我们应用程序的根组件。

<script>
// 1.组件: App组件(根组件)
const App = {
data() {
return {
message: "Hello Vue"
}
}
}
// 1.创建app
const app = Vue.createApp(App)
// 2.挂载app
app.mount("#app")
</script>

任何的应用都可以被抽象成一棵组件树。

image-20230525000510517

03. 注册组件

3.1 全局组件

全局组件注册后,在任何其他的组件中都可以使用的组件;

需要注意的是,全局组件只要注册了,无论有没有使用到这个组件,在使用类似于webpack打包工具去打包项目时,都会对组件进行打包。

在开发中,很少注册全局组件。


示例:注册一个全局组件,用到app.component

<body>
<div id="app">
<!-- 1.内容一: -->
<product-item></product-item>
<!-- 2.内容二: -->
<product-item></product-item>
<!-- 3.内容三: -->
<product-item></product-item>
</div>
<!-- 组件product-item的模板 -->
<template id="item">
<div class="product">
<h2>我是商品</h2>
<div>商品图片</div>
<div>商品价格: <span>¥9.9</span></div>
<p>商品描述信息, 9.9秒杀</p>
</div>
</template>
<script src="../lib/vue.js"></script>
<script>
/*
1.通过app.component(组件名称, 组件的对象)
2.在App组件的模板中, 可以直接使用product-item的组件
*/
// 1.组件: App组件(根组件)
const App = {}
// 2.创建app
const app = Vue.createApp(App)
// 3.注册一个全局组件
// 第一个参数定义组件的名称:product-item;第二参数传入一个对象。
app.component("product-item", {
template: "#item" // 此处也可以直接书写template
})
// 2.挂载app
app.mount("#app")
</script>
</body>

3.2 局部组件

局部组件,只有在注册的组件中才能使用的组件;

通过components属性选项来进行注册;

image-20230525000505960

比如之前的App组件中,我们有data、computed、methods等选项了,事实上还可以有一个components选项;

该components选项对应的是一个对象,对象中的键值对是 组件的名称: 组件对象;

示例:注册局部组件

<body>
<div id="app">
<product-item></product-item>
<product-item></product-item>
<product-item></product-item>
</div>
<template id="product">
<div class="product">
<h2>{{title}}</h2>
<p>商品描述, 限时折扣, 赶紧抢购</p>
<p>价格: {{price}}</p>
<button>收藏</button>
</div>
</template>
<script>
// 1.创建app
const ProductItem = {
template: "#product",
data() {
return {
title: "我是product的title",
price: 9.9
}
}
}
// 1.1.组件打算在哪里被使用
const app = Vue.createApp({
// components: option api
components: {
ProductItem,
}
},
// data: option api
data() {
return {
message: "Hello Vue"
}
}
})
// 2.挂载app
app.mount("#app")
</script>
</body>

3.3 组件名称规范

在通过app.component注册一个组件的时候,第一个参数是组件的名称,定义组件名的方式有两种:

  • 方式一:使用kebab-case(短横线分割符)

当使用 kebab-case (短横线分隔命名) 定义一个组件时,引用这个自定义元素时也必须使用 kebab-case,

例如

  • 方式二:使用PascalCase(驼峰标识符)

当使用 PascalCase (首字母大写命名) 定义一个组件时,在引用这个自定义元素时两种命名法都可以使用。

也就是说 都是可接受的;

04. 脚手架

4.1 开发模式

目前我们使用vue的过程都是在html文件中,通过template编写自己的模板、脚本逻辑、样式等。

这样的开发模式在面对复杂项目的时候会显得很臃肿,不利于维护。

所以在真实开发中,我们会通过一个后缀名为 .vue 的single-file components (单文件组件) 来解决,

并且可以使用webpack或者vite或者rollup等构建工具来对其进行处理。

每一个.vue文件有自己的template、script、style等标签,维护自己的代码逻辑:

image-20230525000500753

如果我们想要使用这一的SFC的.vue文件,比较常见的是两种方式:

  • 方式一:使用Vue CLI来创建项目,项目会默认帮助我们配置好所有的配置选项,可以在其中直接使用.vue文件;

  • 方式二:自己使用webpack或rollup或vite这类打包工具,对其进行打包处理;

4.2 Vue CLI脚手架

脚手架其实是建筑工程中的一个概念,在我们软件工程中也会将一些帮助我们搭建项目的工具称之为脚手架。

Vue的脚手架就是Vue CLI,本质上是一个npm包。

CLI是Command-Line Interface, 翻译为命令行界面.

我们可以通过CLI选择项目的配置和创建出我们的项目;

Vue CLI已经内置了webpack相关的配置,我们不需要从零来配置;

安装Vue CLI

建议全局安装,这样任何时候都可以通过vue命令来创建项目:

npm install @vue/cli -g

查看Vue CLI版本

vue --version

升级Vue CLI

npm update @vue/cli -g

通过Vue命令创建项目

vue create 项目名称

项目创建过程

  1. 选择预设:
  • Vue2预设
  • Vue3预设
  • 手动模式

babel是一个将ES6语法转化成ES5的工具,eslint是做语法检查。

image-20230525000455335

  1. 选择需要的特性

image-20230525000452299

  1. 选择Vue版本

image-20230525000448837

  1. 是否将配置信息存放到单独的文件中

image-20230525000446007

  1. 是否将当前的配置保存为【预设】,可方便下次直接创建

image-20230525000441923

  1. 选择一个包管理工具

image-20230525000437666

  1. 项目创建中

image-20230525000433429

  1. 启动项目,在项目所在目录的终端下,输入npm run serve,即可启动项目。

image-20230525000428076

  1. 项目的目录结构:

image-20230525000422323

  • babel.config.js babel是ES6转换ES5的组件,这是他所需的配置文件(一般不需要动)。
  • package.json 项目所需的包的版本信息。
  • package-lock.json 保存项目所需的包细节以及包的依赖等信息。
  • node-modules 项目安装依赖包的文件保存的地方。例如:npm install axios
    axios包会保存在此目录、信息也会写在 package.json、package-lock.json中
  • src
    • main.js 项目的启动 npm run serve ,用户访问时程序的入门。
    • App.vue 主组件
    • components 子组件
    • assets 静态文件(自己的静态文件,会被压缩和合并)
  • public 【此目录下的文件直接被复制到dist/目录下,一般放不动的数据,引入第三方】
    • index.html 主HTML文件(模板引擎)(主界面)
    • favicon.icon 图标
  • README.md 项目说明文档

4.3 vite脚手架

使用 vue create 项目名称 的方式去创建项目,默认的打包工具是webpack;

我们也可以使用以下的命令创建项目,其打包工具是vite。

这种方式正逐渐成为主流,因为它的打包速度非常快。

npm init vue@latest

项目结构:

image-20230525000417354

启动项目:

npm run dev

05. 父子组件间通信

image-20230525000412683

上面的嵌套逻辑如下,它们存在如下关系:

  • App组件是Header、Main、Footer组件的父组件;
  • Main组件是Banner、ProductList组件的父组件;

在开发过程中,我们会经常遇到需要组件之间相互进行通信:

  • 比如App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要分别给Header传递一些数据,以进行展示;
  • 又比如我们在Main中向服务器一次性请求了Banner数据和ProductList数据,需要传递给它们,以进行展示;
  • 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件;

父子组件之间如何进行通信呢?

  • 父组件传递给子组件:通过props属性;

  • 子组件传递给父组件:通过$emit触发事件;

image-20230525000407321

5.1 父组件传递子组件

在开发中很常见的就是父子组件之间通信,比如父组件有一些数据,需要子组件来进行展示。

这个时候我们可以通过props来完成组件之间的通信;

什么是Props呢?

你可以在组件上注册一些自定义的attribute,这些attribute统称为Props;

父组件给这些attribute赋值,子组件通过attribute的名称获取到对应的值;

Props有两种常见的用法:

  • 方式一:字符串、数组,数组中的字符串就是attribute的名称;

  • 方式二:对象类型,对象类型我们可以在指定attribute名称的同时,指定它需要传递的类型、是否是必须的、默认值等等;

App.vue:

注意下面案例中,以:age="30"形式传入的是数字。

因为v-bind后面引号里的是js代码。

<template>
<!-- 1.展示why的个人信息 -->
<show-info name="why" :age="18" :height="1.88" />
<!-- 2.展示kobe的个人信息 -->
<show-info name="kobe" :age="30" :height="1.87" />
<!-- 3.展示默认的个人信息 -->
<show-info :age="100" show-message="哈哈哈哈"/>
</template>
<script>
import ShowInfo from './ShowInfo.vue'
export default {
components: {
ShowInfo
}
}
</script>
<style scoped>
</style>

showInfo.vue:

<template>
<div class="infos">
<h2>姓名: {{ name }}</h2>
<h2>年龄: {{ age }}</h2>
<h2>身高: {{ height }}</h2>
<h2>Message: {{ showMessage }}</h2>
</div>
<div class="others" v-bind="$attrs"></div>
</template>
<script>
export default {
// 1.props数组语法(不常用)
// 弊端: 1.不能对类型进行验证 2.没有默认值
// props: ["name", "age", "height"]
// 2.props对象语法(必须掌握)
props: {
name: {
type: String,
default: "我是默认name"
},
age: {
type: Number,
required: true, // 是否必须传值,如果为true,则默认值失效。
default: 0
},
height: {
type: Number,
default: 2
},
// 重要的原则: 数组、对象类型写默认值时, 需要编写default的函数, 函数返回默认值
friend: {
type: Object,
default() {
return { name: "james" }
}
},
hobbies: {
type: Array,
default: () => ["篮球", "rap", "唱跳"]
},
// 命名使用小驼峰,
// 父组件中,属性传值可以使用短横杠【-】:<show-info :age="100" show-message="哈哈哈哈"/>
showMessage: {
type: String,
default: "我是showMessage"
}
}
}
</script>

type支持的类型:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

type也支持多个值,以数组的形式:

name: {
type: [String,Number]
default: "我是默认name"
},

对象类型,或者数组类型,其默认值必须是一个函数。由函数来返回对象。

当我们传递给一个组件某个属性,但是该属性并没有定义对应的props或者emits时,就称之为 非Prop的Attribute

常见的包括class、style、id属性等;

当组件有单个根节点时,非Prop的Attribute将自动添加到根节点的Attribute中;没有根节点,则会报警告,提示必须指定要绑定到哪一个标签上。

如果我们不希望组件的根元素继承attribute,可以在组件中设置 inheritAttrs: false

示例:showInfo.vue:

<template>
<!-- address="广州市" abc="cba" class="active" 等属性,并没有定义在props中 -->
<show-info name="why" :age="18" :height="1.88"
address="广州市" abc="cba" class="active" />
</template>
<script>
import ShowInfo from './ShowInfo.vue'
export default {
components: {
ShowInfo
}
}
</script>
<style scoped>
</style>

App.vue:

<template>
<div class="infos">
<h2>姓名: {{ name }}</h2>
<h2>年龄: {{ age }}</h2>
<h2>身高: {{ height }}</h2>
</div>
<div class="others" v-bind="$attrs"></div>
</template>
<script>
export default {
// 如果不希望组件的根元素继承attribute,可以使用下面的配置:
inheritAttrs: false,
props: {
name: {
type: String,
default: "我是默认name"
},
age: {
type: Number,
required: true, // 是否必须传值,如果为true,则默认值失效。
default: 0
},
height: {
type: Number,
default: 2
},
}
}
</script>

image-20230525000355138

如果禁用了attribute继承,我们还是可以通过 $attrs来访问所有的 非props的attribute;

<h2 :class="$attrs.class">姓名: {{ name }}</h2>

5.2 子组件传递父组件

当子组件有一些事件发生的时候,比如在组件中发生了点击,父组件需要切换内容,此时就需要子组件传递信息给父组件。

我们如何完成上面的操作呢?

  • 首先,在子组件中定义好在某些情况下触发的事件名称;

  • 其次,在父组件中以v-on的方式传入要监听的事件名称,并且绑定到对应的方法中;

  • 最后,在子组件中发生某个事件的时候,根据事件名称触发对应的事件;

以下示例实现一个计数功能:

image-20230525000342259

App.vue

<template>
<div class="app">
<h2>当前计数: {{ counter }}</h2>
<!-- 1.自定义add-counter, 并且监听内部的add事件 -->
<add-counter @add="addBtnClick"></add-counter>
<!-- 2.自定义sub-counter, 并且监听内部的sub事件 -->
<sub-counter @sub="subBtnClick"></sub-counter>
</div>
</template>
<script>
import AddCounter from './AddCounter.vue'
import SubCounter from './SubCounter.vue'
export default {
components: {
AddCounter,
SubCounter
},
data() {
return {
counter: 0
}
},
methods: {
addBtnClick(count) {
this.counter += count
},
subBtnClick(count) {
this.counter -= count
}
}
}
</script>

AddCounter.vue

<template>
<div class="add">
<button @click="btnClick(1)">+1</button>
<button @click="btnClick(5)">+5</button>
<button @click="btnClick(10)">+10</button>
</div>
</template>
<script>
export default {
methods: {
btnClick(count) {
console.log("btnClick:", count)
// 让子组件发出去一个自定义事件:
// 第一个参数自定义的事件名称
// 第二个参数是传递的参数
this.$emit("add", count)
}
}
}
</script>

SubCounter.vue

<template>
<div class="sub">
<button @click="btnClick(1)">-1</button>
<button @click="btnClick(5)">-5</button>
<button @click="btnClick(10)">-10</button>
</div>
</template>
<script>
export default {
emits: ["add"], // 写明传递出去的函数名,方便查看与代码提示
methods: {
btnClick(count) {
this.$emit("sub", count)
}
}
}
</script>

image-20230525000333237

在子组件中,定义vue对象时,有一个emits属性。

emits的数组写法:

  • 可以方便查看该组件都定义了哪些自定义事件,
  • 如果有emits,父组件在书写代码时,也会有事件名称提示。

emmits对象语法:

  • 事件名称对应的是函数,当触发这个事件时,先执行此函数。
  • 常用于在参数被传递出去前做验证
<script>
export default {
// 1.emits数组语法
emits: ["add"],
// 2.emmits对象语法
// emits: {
// add: function(count) {
// if (count <= 10) {
// return true
// }
// return false
// }
// },
methods: {
btnClick(count) {
console.log("btnClick:", count)
// 让子组件发出去一个自定义事件
// 第一个参数自定义的事件名称
// 第二个参数是传递的参数
this.$emit("add", 100)
}
}
}
</script>

案例:组件通信

效果图

image-20230527234846040

App.vue

<template>
<div class="app">
<!-- 1.tab-control -->
<tab-control :titles="['衣服', '鞋子', '裤子']" @tab-item-click="tabItemClick"/>
<!-- 2.展示内容 -->
<h1>{{ pageContents[currentIndex] }}</h1>
</div>
</template>
<script>
import TabControl from './TabControl.vue'
export default {
components: {
TabControl
},
data() {
return {
pageContents: [ "衣服列表", "鞋子列表", "裤子列表" ],
currentIndex: 0
}
},
methods: {
tabItemClick(index) {
console.log("app:", index)
this.currentIndex = index
}
}
}
</script>

TabControl.vue

<template>
<div class="tab-control">
<template v-for="(item, index) in titles" :key="item">
<div class="tab-control-item"
:class="{ active: index === currentIndex }"
@click="itemClick(index)">
<span>{{ item }}</span>
</div>
</template>
</div>
</template>
<script>
export default {
props: {
titles: {
type: Array,
default: () => []
}
},
data() {
return {
currentIndex: 0
}
},
emits: ["tabItemClick"],
methods: {
itemClick(index) {
this.currentIndex = index
this.$emit("tabItemClick", index)
}
}
}
</script>
<style scoped>
.tab-control {
display: flex;
height: 44px;
line-height: 44px;
text-align: center;
}
.tab-control-item {
flex: 1;
}
.tab-control-item.active {
color: red;
font-weight: 700;
}
.tab-control-item.active span {
border-bottom: 3px solid red;
padding: 8px;
}
</style>

06. 非父子组件通信

在开发中,我们构建了组件树之后,除了父子组件之间的通信之外,还会有非父子组件之间的通信。

这里我们主要讲两种方式:

  • Provide/Inject;
  • 全局事件总线;

6.1 Provide/Inject

Provide/Inject用于非父子组件之间共享数据。

比如有一些深度嵌套的组件,子组件想要获取父组件的部分内容;

在这种情况下,如果我们仍然将props沿着组件链逐级传递下去,就会非常的麻烦。

对于这种情况下,我们可以使用 Provide 和 Inject :

  • 无论层级结构有多深,父组件都可以作为其所有子组件的依赖提供者;

  • 父组件有一个 provide 选项来提供数据;

  • 子组件有一个 inject 选项来开始使用这些数据;

image-20230525160000788

这种传递方式:

父组件不需要知道哪些子组件使用它 provide 的 property;

子组件不需要知道 inject 的 property 来自哪里。

image-20230525161354741

image-20230525161743858

provide也支持对象写法。

但如果Provide中提供的一些数据是来自data,那么我们可能会想要通过this来获取,这个时候可能会报错。

<script>
import Home from './Home.vue'
export default {
components: {
Home
},
created() {
},
data() {
return {
message: "Hello App"
}
},
provide: {
name: "why",
age: 18,
message: this.message // 这种写法会报错,因为对象没有作用域
}
}
</script>

为了防止出现类似的错误,对于provide我们一般使用函数写法。函数就由自己的作用域。

<script>
import Home from './Home.vue'
export default {
components: {
Home
},
created() {
},
data() {
return {
message: "Hello App"
}
},
// provide一般都是写成函数,函数有自己的作用域,this就可以访问到data中的数据
provide() {
return {
name: "why",
age: 18,
message: this.message
}
}
}
</script>

如果需要响应式数据,可以使用computed函数。

以下案例provide选项中传递了message,而页面中存在修改message按钮,该按钮修改message的值。

如果按照上面的写法,provide选项中直接传递message: this.message,message不会是响应式的。

此时使用computed函数,就可以将数据 变成响应式的了。

computed函数要求传入一个函数。此处必须使用箭头函数,箭头函数中的this访问的是上一层中的this,如此才能访问到data中的数据。

<template>
<div class="app">
<home></home>
<h2>App: {{ message }}</h2>
<button @click="message = 'hello world'">修改message</button>
</div>
</template>
<script>
import { computed } from 'vue'
import Home from './Home.vue'
export default {
components: {
Home
},
created() {
},
data() {
return {
message: "Hello App"
}
},
// provide一般都是写成函数
provide() {
return {
name: "why",
age: 18,
// 注意computed函数中,必须传递箭头函数。
message: computed(() => this.message)
}
}
}
</script>

需要注意的是:使用computed函数,取值时需要自己解包。

image-20230525161316117

6.2 全局事件总线mitt库

provide/inject适用于祖先组件给子孙组件传递信息。

但在开发过程中,我们也会遇到同级别的组件之间需要传递信息。

这时候就可以用事件总线

Vue3从实例中移除了 $on、$off 和 $once 方法,

所以我们如果希望继续使用全局事件总线,要通过第三方的库:

Vue3官方有推荐一些库,例如 mitt 或 tiny-emitter;

不过这两个库并不是官方维护的,已经很久不更新了。

这里我们主要讲解一下 hy-event-store。

安装:

npm install hy-event-bus

安装完成后,在项目中使用。

image-20230525182638641

第一步:创建utils/event-bus.js:

import {HYEventBus} from 'hy-event-store'
const eventBus = new HYEventBus()
export default eventBus

第二步:在需要传递信息的组件中,导入utils/event-bus.js中的实例对象eventBus,调用emit方法传递。

以下案例实现:在HomeBanner组件中定义了按钮 banner按钮 ,当这个按钮被点击时,除了发生自身代码逻辑外,同时也将事件传递给其他组件。

HomeBanner.vue

<template>
<div class="banner">
<button @click="bannerBtnClick">banner按钮</button>
</div>
</template>
<script>
import eventBus from './utils/event-bus'
export default {
methods: {
bannerBtnClick() {
console.log("bannerBtnClick")
// 第一个参数为事件名称,后面均为参数。
eventBus.emit("whyEvent", "why", 18, 1.88)
}
}
}
</script>

第三步:在需要接收信息的组件,调用on方法。

  • 在App.vue中监听:HomeBanner组件发生whyEvent事件时,自身修改数据。
<template>
<div class="app">
<h2>App Message: {{ message }}</h2>
</div>
</template>
<script>
import eventBus from './utils/event-bus'
import Category from './Category.vue'
export default {
components: {
Home,
Category
},
data() {
return {
message: "Hello App",
}
},
created() {
// 事件监听:eventBus.on接收两个参数:第一参数是监听的事件名称,第二个参数是该执行函数名称
// 在这里,因为需要监听到事件发生后,需要修改data中的数据,所以使用了箭头函数。
eventBus.on("whyEvent", (name, age, height) => {
console.log("whyEvent事件在app中监听", name, age, height)
this.message = `name:${name}, age:${age}, height:${height}`
})
}
}
</script>
  • 在Category.vue中监听:HomeBanner组件发生whyEvent事件时,自身执行whyEventHandler函数。
<template>
<div>
<h2>Category</h2>
</div>
</template>
<script>
import eventBus from './utils/event-bus'
export default {
methods: {
whyEventHandler() {
console.log("whyEvent在category中监听")
}
},
created() {
eventBus.on("whyEvent", this.whyEventHandler)
},
}
</script>

某些业务逻辑下,祖先组件会移除组件,如果被移除的组件有使用全局事件总线监听某个值,此时也应该一并移除监听事件。

可使用组件的unmounted属性进行移除。

eventBus.off("whyEvent", this.whyEventHandler) 第一的参数传入监听的事件名称,第二个参数传入函数名。

<script>
import eventBus from './utils/event-bus'
export default {
methods: {
whyEventHandler() {
console.log("whyEvent在category中监听")
}
},
created() {
eventBus.on("whyEvent", this.whyEventHandler)
},
unmounted() {
console.log("category unmounted")
eventBus.off("whyEvent", this.whyEventHandler)
}
}
</script>

07. 动态组件

动态组件是使用<component>标签,通过一个特殊的 attribute --> is 来实现:is=组件名称

这个currentTab的值需要是什么内容呢?

  • 全局注册:可以是通过component函数注册的组件;

  • 局部注册:在一个组件对象的components对象中注册的组件.

组件间的通信与之前类似。

<template>
<div class="app">
<div class="tabs">
<template v-for="(item, index) in tabs" :key="item">
<button :class="{ active: currentTab === item }"
@click="itemClick(item)">
{{ item }}
</button>
</template>
</div>
<div class="view">
<component name="why"
:age="18"
@homeClick="homeClick"
:is="currentTab">
</component>
</div>
</div>
</template>
<script>
import Home from './views/Home.vue'
import About from './views/About.vue'
import Category from './views/Category.vue'
export default {
components: {
Home,
About,
Category
},
data() {
return {
tabs: ["home", "about", "category"],
currentTab: "home"
}
},
methods: {
itemClick(tab) {
this.currentTab = tab
},
homeClick(payload) {
console.log("homeClick:", payload)
}
}
}
</script>
<style scoped>
.active {
color: red;
}
</style>

08. 插槽slot

如果要让组件在不同的使用场景下显示不同的数据,我们可以用props来传递;

但如果我们要求,组件在不同的使用场景下,内部元素有所不同呢?

p标签是一个元素,img标签是另一个元素。

比如,某些场景下使用这个组件,我们希望某个元素是图片,另一个场景下使用这个组件,我们希望某个元素是按钮。

举个例子,假如我们要设计一个导航组件,

这个组件分成三块区域:左边-中间-右边,每块区域的内容是不固定;

  • 左边区域可能显示一个菜单图标,也可能显示一个返回按钮;

  • 中间区域可能显示一个搜索框,也可能是一个列表,也可能是一个标题;

  • 右边可能是一个文字,也可能是一个图标,也可能什么都不显示;

image-20230525140242885

这个时候我们就可以定义插槽slot:

  • 插槽的使用过程其实是抽取共性、预留不同;

  • 我们会将共同的元素、内容依然在组件内进行封装;

  • 同时会将不确定的元素使用slot作为占位,使用该组件时再决定显示什么元素;

如何使用slot呢?

  • Vue中将<slot>元素作为承载分发内容的出口;

  • 在封装组件中,使用特殊的元素<slot>就可以为组件开启一个插槽;

  • 该插槽插入什么内容取决于父组件如何使用;

8.1 基本使用

image-20230525141425575

App.vue

<template>
<div class="app">
<!-- 1.内容是button -->
<show-message title="哈哈哈">
<button>我是按钮元素</button>
</show-message>
<!-- 2.内容是超链接 -->
<show-message>
<a href="#">百度一下</a>
</show-message>
<!-- 3.内容是一张图片 -->
<show-message>
<img src="@/img/kobe02.png" alt="">
</show-message>
<!-- 4.内容没有传递,使用默认值 -->
<show-message></show-message>
</div>
</template>
<script>
import ShowMessage from './ShowMessage.vue'
export default {
components: {
ShowMessage
}
}
</script>

ShowMessage.vue

<template>
<h2>{{ title }}</h2>
<div class="content">
<slot>
<p>我是插槽默认内容</p>
</slot>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: "我是title默认值"
}
}
}
</script>

8.2 具名插槽

image-20230525142600228

App.vue

<template>
<nav-bar>
<template #left> <!-- 多个插槽时,需要使用template标签 -->
<button>{{ leftText }}</button>
</template>
<template #center>
<span>内容</span>
</template>
<template v-slot:right>
<a href="#">登录</a>
</template>
</nav-bar>
<nav-bar>
<template v-slot:[position]> <!-- v-slot绑定的值也可以是变量 -->
<a href="#">注册</a>
</template>
</nav-bar>
<button @click=" position = 'left' ">左边</button>
<button @click=" position = 'center' ">中间</button>
<button @click=" position = 'right' ">右边</button>
</template>
<script>
import NavBar from './NavBar.vue'
export default {
components: {
NavBar
},
data() {
return {
position: "center",
leftText: "返回"
}
}
}
</script>

NavBar.vue

<template>
<div class="nav-bar">
<div class="left">
<slot name="left">left</slot>
</div>
<div class="center">
<slot name="center">center</slot>
</div>
<div class="right">
<slot name="right">right</slot>
</div>
</div>
<div class="other">
<slot name="default"></slot>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
.nav-bar {
display: flex;
height: 44px;
line-height: 44px;
text-align: center;
}
.left {
width: 80px;
background-color: orange;
}
.center {
flex: 1;
background-color: skyblue;
}
.right {
width: 80px;
background-color: aquamarine;
}
</style>

8.3 插槽作用域

在Vue中有渲染作用域的概念:

  • 父级模板里的所有内容都是在父级作用域中编译的;

  • 子模板里的所有内容都是在子作用域中编译的;

也就是说,在父组件中,默认没办法访问子组件的变量。

所以在前面的知识点中,当父组件需要传递数据给子组件时,我们使用props来传递;

当子组件需要传递数据给父组件时,我们使用$emit,由事件来通信。


如果在使用插槽的时候,父组件需要使用子组件传递信息,该怎么办呢?

此时没办法使用$emit,因为可能不存在任何事件。

举个例子,

在【组件通信案例】中,子组件TabControl.vue里,对于导航条的渲染是<span>标签。

image-20230525150330920

image-20230525150708015

如果我们要优化这个组件,让其默认是<span>标签,父组件使用时也可以自定义其他标签,我们可以将之设计成一个插槽:

image-20230525151146697

这里出现了一个问题,子组件虽然设计了插槽,但父组件在替换插槽时,没法访问到子组件中的item值。

image-20230525151627930

此时,就需要组件间的来传递信息。

插槽的信息传递,与 5.1 提到的方法类似,通过一个属性传递。

image-20230527235319220

如果我们的插槽是默认插槽default,那么在使用的时候 v-slot:default="slotProps"可以简写为v-slot="slotProps";

并且如果我们的插槽只有默认插槽时,可以省略template标签,组件的标签可以被当做插槽的模板来使用,这样,我们就可以将 v-slot 直接用在组件上。

09. 生命周期

什么是生命周期?

生物学上,生物生命周期指得是一个生物体在生命开始到结束周而复始所历经的一系列变化过程;

每个组件都可能会经历从创建、挂载、更新、卸载等一系列的过程;

在这个过程中的某一个阶段,我们可能会想要添加一些属于自己的代码逻辑(比如组件创建完后就请求一些服务器数据);

但是我们如何可以知道目前组件正在哪一个过程呢?Vue给我们提供了组件的生命周期函数;

生命周期函数:

生命周期函数是一些钩子函数(回调函数),在某个时间会被Vue源码内部进行回调;

通过对生命周期函数的回调,我们可以知道目前组件正在经历什么阶段;

那么我们就可以在该生命周期中编写属于自己的逻辑代码了;

Vue组件四个阶段:

  • 初始化 ==> beforeCreate / created
  • 挂载 ==> beforeMount / mouted
  • 更新 ==> beforeUpdate / updated
  • 销毁 ==> beforeDestory / destoryed

324bf45b9d4a9eab71f0b202de7d453

八个周期函数:

<script>
import Home from "./Home.vue"
export default {
components: {
Home
},
data() {
return {
message: "Hello App",
counter: 0,
isShowHome: true
}
},
// 1.组件被创建之前
beforeCreate() {
console.log("beforeCreate")
},
// 2.组件被创建完成
created() {
console.log("created")
console.log("1.发送网络请求, 请求数据")
console.log("2.监听eventbus事件")
console.log("3.监听watch数据")
},
// 3.组件template准备被挂载
beforeMount() {
console.log("beforeMount")
},
// 4.组件template被挂载: 虚拟DOM -> 真实DOM
mounted() {
console.log("mounted")
console.log("1.获取DOM")
console.log("2.使用DOM")
},
// 5.数据发生改变
// 5.1. 准备更新DOM
beforeUpdate() {
console.log("beforeUpdate")
},
// 5.2. 已更新DOM
updated() {
console.log("updated")
},
// 6.卸载VNode -> DOM元素
// 6.1.卸载之前
beforeUnmount() {
console.log("beforeUnmount")
},
// 6.2.DOM元素被卸载完成
unmounted() {
console.log("unmounted")
}
}
</script>

在开发中,某些情况我们希望持续保持组件的状态,而不是销毁掉,这个时候我们就可以使用一个内置组件:keep-alive。

比如在前面的07.动态组件中,我们使用的案例:

image-20230525203748197

如果我们希望在切换about组件,或者category组件时,home组件不会被销毁,则可以用<keep-alive include="home">标签将之包裹。

<!-- include: 组件的名称来自于组件定义时name选项 -->
<keep-alive include="home">
<component :is="currentTab"></component>
</keep-alive>

上面代码中,include="home",组件的名称是定义在nama属性中的。

image-20230525204014592

注意:

如果多个组件需要keep alive,以逗号分隔,中间不能有空格。

include="home,about"

keep-alive有一些属性:

  • include - string | RegExp | Array。只有名称匹配的组件会被缓存;

  • exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存;

  • max - number | string。最多可以缓存多少组件实例,一旦达到这个数字,那么缓存组件中最近没有被访问的实例会被销毁;

include 和 exclude prop 允许组件有条件地缓存:

  • 二者都可以用逗号分隔字符串、正则表达式或一个数组来表示;

  • 匹配首先检查组件自身的 name 选项;

image-20230525204416334

对于缓存的组件来说,再次进入时,我们是不会执行created或者mounted等生命周期函数的:

有时候我们希望 监听 何时重新进入到该组件,何时离开该组件;

这个时候我们可以使用activated 和 deactivated 这两个生命周期钩子函数来监听;

image-20230525204604509

10. $ref的使用

在Vue开发中,是不推荐进行DOM原生操作的;

但某些情况下,我们在组件中确确实实想要直接获取到元素对象或者子组件实例;

这个时候,我们可以给元素或者组件绑定一个ref的attribute属性;

组件实例有一个$refs属性:

它一个对象Object,持有注册过 ref attribute 的所有 DOM 元素和组件实例。

<template>
<div class="app">
<h2 ref="title" class="title" :style="{ color: titleColor }">{{ message }}</h2>
<button ref="btn" @click="changeTitle">修改title</button>
<banner ref="banner"/>
</div>
</template>
<script>
import Banner from "./Banner.vue"
export default {
components: {
Banner
},
data() {
return {
message: "Hello World",
titleColor: "red"
}
},
methods: {
changeTitle() {
// 1.不要主动获取DOM, 修改DOM内容;
// this.message = "你好啊, 李银河!"
// this.titleColor = "blue"
// 2.获取h2/button元素
console.log(this.$refs.title)
console.log(this.$refs.btn)
// 3.获取banner组件: 组件实例
console.log(this.$refs.banner)
// 3.1.在父组件中可以主动调用子组件的对象方法
this.$refs.banner.bannerClick()
// 3.2.获取banner组件实例, 获取banner中的元素,拿到的是根元素。
console.log(this.$refs.banner.$el)
// 3.3.如果banner template是多个根, 拿到的是第一个node节点
// 注意: 开发中不推荐一个组件的template中有多个根元素
// console.log(this.$refs.banner.$el.nextElementSibling)
// 4.组件实例还有两个属性(了解):
console.log(this.$parent) // 获取父组件
console.log(this.$root) // 获取根组件
}
}
}
</script>

Banner.vue:

<template>
<div class="banner">
<h2>Banner</h2>
</div>
</template>
<script>
export default {
created() {
},
methods: {
bannerClick() {
console.log("bannerClick")
}
}
}
</script>

11. webpack代码分包

打包过程中:

默认情况下,在构建整个组件树的过程中,因为组件和组件之间是通过模块化直接依赖的,

那么webpack在打包时就会将组件模块打包到一起(比如一个app.js文件中)。

随着项目的不断庞大,app.js文件的内容过大,会造成首屏的渲染速度变慢的问题。

这时,我们可以考虑在打包时,对代码进行分包:

对于一些不需要立即使用的组件,我们可以单独对它们进行拆分,拆分成一些小的代码块chunk.js;

这些chunk.js会在需要时才从服务器加载下来,从而提高浏览器加载的速度。

那么webpack中如何可以对代码进行分包呢?

对于js文件,使用import的方式导入:

image-20230525205922846

对于组件,也就是.vue文件,Vue提供了一个函数:defineAsyncComponent。

defineAsyncComponent接受两种类型的参数:

  • 类型一:工厂函数,该工厂函数需要返回一个Promise对象;
  • 类型二:接受一个对象类型,对异步函数进行配置;

image-20230525210149229

image-20230525210231287

12. Composition API

在Vue2中,编写组件的方式是Options API。

Options API的一大特点就是在对应的属性中编写对应的功能模块:

  • 在data定义数据
  • 在methods中定义方法
  • 在computed中定义计算属性
  • 在watch中监听属性改变,也包括生命周期钩子;

这种编程方式曾很受欢迎,但随着前端的发展,前端的代码逻辑越来越复杂、庞大,

这种编程方式的弊端就逐渐显现出来:

  • 当我们要实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中;

  • 当组件变得更复杂时,同一个功能的逻辑就会被拆分得很散,这影响了代码的可读性,也不利于维护。

Composition API要解决的事情就是:将同一个逻辑关注点相关的代码收集在一起。

也有人把Vue Composition API简称为VCA。

12.1 Setup函数

Vue组件中,提供了 setup 函数来实现这一点。

Setup其实就是组件的另外一个选项:

只不过这个选项强大到我们可以用它来替代之前所编写的大部分其他选项;

比如methods、computed、watch、data、生命周期等等;

Setup函数的参数

我们先来研究setup函数的参数,它主要有两个参数:

  • 第一个参数:props

  • 第二个参数:context

props非常好理解,父组件传递过来的属性会被放到props对象中,我们在setup中如果需要使用,那么就可以直接通过props参数获取:

对于定义props的类型,我们还是和之前的规则是一样的,在props选项中定义;

并且在template中依然是可以正常去使用props中的属性;

另外一个参数是context,我们也称之为是一个SetupContext,它里面包含三个属性:

  • attrs:所有的非prop的attribute;

  • slots:父组件传递过来的插槽;

  • emit:当我们组件内部需要发出事件时会用到emit;

Setup函数的返回值

setup既然是一个函数,那么它也可以有返回值,它的返回值用来做什么呢?

setup的返回值可以在模板template中被使用,

也就是说我们可以通过setup的返回值来替代data选项;

我们也可以返回一个执行函数来代替在methods中定义的方法:

12.2 小案例:计数功能

我们尝试用Setup函数实现一个计数功能。

App.vue:

<template>
<div class="app">
<!-- template中ref对象会自动解包 -->
<h2>当前计数: {{ counter }}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</div>
</template>
<script>
import {ref} from 'vue'
export default {
setup() {
// 1.定义counter的内容
// 默认定义的数据都不是响应式数据,需要通过ref函数将之转换成响应式数据
let counter = ref(100)
const increment = () => {
counter.value++
console.log(counter.value)
}
const decrement = () => {
counter.value--
}
return {
counter,
increment,
decrement,
}
}
}
</script>

在这个案例中:

  • 我们可以直接定义的数据,代替了以前的data选项;
  • 不过定义的数据,默认都不是响应式数据,需要通过ref函数将之转换成响应式数据;
  • 我们定义的函数,直接通过函数返回值传递出去,代替了以前的method选项。

既然Setup是一个函数,我们也可以将其中的代码逻辑单独抽取称为一个文件:

hook/useCount.js:

import { ref } from 'vue'
export default function useCounter() {
let counter = ref(100)
const increment = () => {
counter.value++
console.log(counter.value)
}
const decrement = () => {
counter.value--
}
return { counter, increment, decrement }
}

App.vue

<template>
<div class="app">
<!-- template中ref对象自动解包 -->
<h2>当前计数: {{ counter }}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</div>
</template>
<script>
import useCounter from './hooks/useCounter'
export default {
setup() {
// 方式一:
// const { counter, increment, decrement } = useCounter()
// return {
// counter,
// increment,
// decrement,
// }
// 方式二:...是语法糖,直接解包。
return {
...useCounter()
}
}
}
</script>

12.3 响应式数据

在Setup函数中,定义的数据默认都不是响应式数据,前面我们通过ref函数将之转换成响应式数据。

在CompositionAPI中,想为在setup中定义的数据提供响应式的特性,有两种做法:

  • 通过reactive函数:用于定义复杂数据
    • 需要传入对象、数组类型;
    • 如果传入基本数据类型(String、Number、Boolean),会报一个警告;
  • 通过ref函数:用于定义简单数据,也可以定义复杂数据
    • ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是ref名称的来源;
    • 它内部的值是在ref的 value 属性中被维护的;
    • 在模板中引入ref的值时,Vue会自动帮助我们进行解包操作(自动取出其中的value),所以我们并不需要在模板中通过 ref.value 的方式来使用;
    • 在 setup 函数内部,它依然是一个 ref引用, 所以对其进行操作时,我们依然需要使用 ref.value的方式;

在开发中,对于reactive和ref函数的使用,习惯上会做如下区分。

reactive的使用场景:

  • 应用于本地数据,而不是从服务器获取;
  • 多个数据之间有关系,也就是聚合数据,比如用户名与密码等,使用reactive函数,将之统一带一个对象中。

其他场景则使用ref函数。

为什么使用reactive函数就可以变成响应式的呢?

这是因为当我们使用reactive函数处理我们的数据时,数据再次被使用时就会进行依赖收集;

当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面);

事实上,以前我们编写的data选项,也是在内部交给了reactive函数将其编程响应式对象的;

<template>
<div>
<h2>message: {{ message }}</h2>
<button @click="changeMessage">修改message</button>
<hr>
<h2>账号: {{ account.username }}</h2>
<h2>密码: {{ account.password }}</h2>
<button @click="changeAccount">修改账号</button>
<hr>
<!-- 默认情况下在template中使用ref时, vue会自动对其进行解包(取出其中value) -->
<h2>当前计数: {{ counter }}</h2>
<button @click="increment">+1</button>
<!-- 仍然可以直接修改值 -->
<button @click="counter++">+1</button>
<hr>
<!-- 浅层解包:以前使用的时候需要写.value ;新版已优化-->
<h2>当前计数: {{ info.counter }}</h2>
<!-- 但修改的时候就需要写.value ,即修改时仍是浅层解包-->
<button @click="info.counter.value++">+1</button>
</div>
</template>
<script>
import { reactive, ref } from 'vue'
export default {
setup() {
// 1.定义普通的数据: 可以正常被使用
// 缺点: 数据不是响应式的
let message = "Hello World"
function changeMessage() {
message = "你好啊,李银河!"
console.log(message) // 页面数据并没有改变
}
// 2.定义响应式数据
// 2.1.reactive函数: 定义复杂类型的数据
const account = reactive({
username: "coderwhy",
password: "123456"
})
function changeAccount() {
account.username = "kobe"
}
// 2.2.ref函数: 定义简单类型的数据(也可以定义复杂类型的数据)
// counter定义响应式数据
const counter = ref(0)
function increment() {
counter.value++
}
// 3.ref是浅层解包(新版已经更改,但仍有问题)
const info = {
counter
}
return {
message,
changeMessage,
account,
changeAccount,
counter,
increment,
info
}
}
}
</script>

12.4 单向数据流

在Vue的开发规范中,有一个“单向数据流”的概念。

意思是:当父组件给子组件传递一些数据的时候,子组件不应该去修改它;

如果子组件确实需要修改数据的值,应该通过事件的方式传递给父组件,由父组件去修改。

为什么要这么做呢?

因为在项目开发中,父组件常常是将数据同时传递给多个子组件的,如果子组件去修改数据的值,对于父组件来收,它并不知道是那个子组件修改了值,这可能造成数据混乱。


不规范做法:

App.vue

<template>
<h2>App: {{ info }}</h2>
<show-info :info="info"></show-info>
</template>
<script>
import { reactive } from 'vue'
import ShowInfo from './ShowInfo.vue'
export default {
components: {
ShowInfo
},
setup() {
// 本地定义多个数据, 都需要传递给子组件
// name/age/height
const info = reactive({
name: "why",
age: 18,
height: 1.88
})
return {
info,
}
}
}
</script>

ShowInfo.vue

<template>
<div>
<h2>ShowInfo: {{ info }}</h2>
<!-- 代码没有错误, 但是违背规范(单项数据流) -->
<button @click="info.name = 'kobe'">ShowInfo按钮</button>
</div>
</template>
<script>
export default {
props: {
// reactive数据
info: {
type: Object,
default: () => ({})
},
}
}
</script>

正确做法:

App.vue

<template>
<h2>App: {{ info }}</h2>
<show-info :Info="Info" @changeInfoName="changeInfoName"></show-info>
</template>
<script>
import { reactive } from 'vue'
import ShowInfo from './ShowInfo.vue'
export default {
components: {
ShowInfo
},
setup() {
// 本地定义多个数据, 都需要传递给子组件
// name/age/height
const info = reactive({
name: "why",
age: 18,
height: 1.88
})
function changeInfoName(payload) {
info.name = payload
}
return {
info,
changeInfoName,
}
}
}
</script>

ShowInfo.vue

<template>
<div>
<!-- 正确的做法: 符合单向数据流-->
<button @click="showInfobtnClick">ShowInfo按钮</button>
<hr>
</div>
</template>
<script>
export default {
props: {
// reactive数据
info: {
type: Object,
default: () => ({})
},
},
emits: ["changeInfoName"],
setup(props, context) {
function showInfobtnClick() {
// 注意这里是使用Setup参数context,由context.emit来传递事件
context.emit("changeInfoName", "kobe")
}
return {
showInfobtnClick,
}
}
}
</script>

对于单向数据量的规范,Vue以前的版本并没有进行语法上的规定。

到了Vue3,Vue提供了readonly的方法,从语法上做了限制。

readonly会返回原生对象的只读代理(也就是它依然是一个Proxy,这个proxy的set方法被劫持,并且不能对其进行修改);

<template>
<h2>App: {{ info }}</h2>
<show-info :roInfo="roInfo" @changeRoInfoName="changeRoInfoName">
</show-info>
</template>
<script>
import { reactive, readonly } from 'vue'
import ShowInfo from './ShowInfo.vue'
export default {
components: {
ShowInfo
},
setup() {
const info = reactive({
name: "why",
age: 18,
height: 1.88
})
// 使用readOnly包裹info
const roInfo = readonly(info)
function changeRoInfoName(payload) {
info.name = payload
}
return {
info,
changeInfoName,
roInfo,
changeRoInfoName
}
}
}
</script>

ShowInfo.vue

<template>
<div>
<!-- 使用readonly的数据 -->
<h2>ShowInfo: {{ roInfo }}</h2>
<!-- 由于roInfo已经被设置成readonly,所以代码会失效(报警告) -->
<!-- <button @click="roInfo.name = 'james'">ShowInfo按钮</button> -->
<!-- 正确的做法 -->
<button @click="roInfoBtnClick">roInfo按钮</button>
</div>
</template>
<script>
export default {
props: {
// readonly数据
roInfo: {
type: Object,
default: () => ({})
}
},
emits: ["changeRoInfoName"],
setup(props, context) {
function roInfoBtnClick() {
context.emit("changeRoInfoName", "james")
}
return {
roInfoBtnClick
}
}
}
</script>

在开发中常见的readonly方法会传入三个类型的参数:

  • 类型一:普通对象;

  • 类型二:reactive返回的对象;

  • 类型三:ref的对象;

12.5 常见API

toRef

如果我们使用ES6的解构语法,对reactive返回的对象进行解构去获取值,那么之后无论是修改结构后的变量,还是修改reactive返回的state对象,数据都不再是响应式的了。

const info = reactive({
name: 'zibuyu',
age: 18
})
const {name, age} = info // reactive被解构后会变成普通的值, 失去响应式

那么有没有办法让我们解构出来的属性是响应式的呢?

Vue为我们提供了一个toRefs的函数,可以将reactive返回的对象中的属性都转成ref;

那么我们再次进行结构出来的 name 和 age 本身都是 ref 的;

<script>
import {reactive, toRefs, toRef} from 'vue'
export default {
setup() {
const info = reactive({
name: "zibuyu",
age: 18,
height: 1.88
})
// 使用toRef,保留响应式:toRefs结构多个值,toRef结构单一值
const {name, age} = toRefs(info)
const height = toRef(info, "height")
return {
name,
age,
height
}
}
}
</script>

与Reactive相关的API

isProxy:检查对象是否是由 reactive 或 readonly创建的 proxy。

isReactive:检查对象是否是由 reactive创建的响应式代理;如果该代理是 readonly 建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true;

isReadonly:检查对象是否是由 readonly 创建的只读代理。

toRaw:返回 reactive 或 readonly 代理的原始对象(不建议保留对原始对象的持久引用。请谨慎使用)。

shallowReactive:创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)。

shallowReadonly:创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)。

与ref相关的API

unref:如果我们想要获取一个ref引用中的value,那么也可以通过unref方法:

如果参数是一个 ref,则返回内部值,否则返回参数本身;

这是 val = isRef(val) ? val.value : val 的语法糖函数;

isRef:判断值是否是一个ref对象。

shallowRef:创建一个浅层的ref对象;

triggerRef:手动触发和 shallowRef 相关联的副作用:

12.6 computed

在前面我们讲解过计算属性computed:当我们的某些属性是依赖其他状态时,我们可以使用计算属性来处理。

在前面的Options API中,我们是使用computed选项来完成的;

在Composition API中,我们可以在 setup 函数中使用 computed 方法来编写一个计算属性;

如何使用computed呢?

方式一:接收一个getter函数,并为 getter 函数返回的值,返回一个不变的 ref 对象;

方式二:接收一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象;

<template>
<h2>{{ fullname }}</h2>
<button @click="setFullname">设置fullname</button>
<h2>{{ scoreLevel }}</h2>
</template>
<script>
import { reactive, computed, ref } from 'vue'
export default {
setup() {
// 1.定义数据
const names = reactive({
firstName: "kobe",
lastName: "bryant"
})
// 方式一:
// const fullname = computed(() => {
// return names.firstName + " " + names.lastName
// })
// 方式二:
const fullname = computed({
set: function(newValue) {
const tempNames = newValue.split(" ")
names.firstName = tempNames[0]
names.lastName = tempNames[1]
},
get: function() {
return names.firstName + " " + names.lastName
}
})
function setFullname() {
fullname.value = "coder why"
console.log(names)
}
// 2.定义score
const score = ref(89)
const scoreLevel = computed(() => {
return score.value >= 60 ? "及格": "不及格"
})
return {
names,
fullname,
setFullname,
scoreLevel
}
}
}
</script>

12.7 在Setup中使用ref

在optionAPI中,如果我们要获取元素或组件,我们的方法是,使用ref注册,继而在this.$ref获取。

<template>
<h2 ref="title">我是标题</h2>
<button ref="btn">我是按钮</button>
</template>
<script>
export default {
mounted() {
console.log(this.$refs.title)
console.log(this.$refs.btn)
}
}
</script>

那么,使用compositionAPI,在setup中如何使用ref获取元素或者组件?

其实非常简单,我们只需要定义一个ref对象,绑定到元素或者组件的ref属性上即可。

  1. 从vue中导入ref;import { ref } from 'vue'
  2. 实例化ref:const titleRef = ref()
  3. 将实例化的ref绑定到对应的元素或组件中:<h2 ref="titleRef">我是标题</h2>
<template>
<!-- 1.获取元素 -->
<h2 ref="titleRef">我是标题</h2>
<button ref="btnRef">按钮</button>
<!-- 2.获取组件实例 -->
<show-info ref="showInfoRef"></show-info>
<button @click="getElements">获取元素</button>
</template>
<script>
import { ref, onMounted } from 'vue'
import ShowInfo from './ShowInfo.vue'
export default {
components: {
ShowInfo
},
setup() {
const titleRef = ref()
const btnRef = ref()
const showInfoRef = ref()
// mounted的生命周期函数
onMounted(() => {
console.log(titleRef.value)
console.log(btnRef.value)
console.log(showInfoRef.value)
showInfoRef.value.showInfoFoo()
})
function getElements() {
console.log(titleRef.value)
}
return {
titleRef,
btnRef,
showInfoRef,
getElements
}
}
}
</script>

12.8 生命周期

在optionAPI中,有八个生命周期函数。

而在compositionAPI中,涉及到生命周期,需要导入 onX 函数注册生命周期钩子。

optionAPI Hook inside Setup
beforeCreate Not needed
created Not needed
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmounted onBeforeUnmounted
unmounted onUnmounted
activated onActivated
deactivated onDeactivated
<template>
<div>AppContent</div>
</template>
<script>
import { onMounted, onUpdated, onUnmounted } from 'vue'
export default {
// optionAPI 的写法:
// beforeCreate() {
// },
// created() {
// },
// beforeMount() {
// },
// mounted() {
// },
// beforeUpdate() {
// },
// updated() {
// }
setup() {
// 在执行setup函数的过程中, 你需要注册别的生命周期函数
onMounted(() => {
console.log("onmounted")
})
}
}
</script>

12.8 Provide函数

我们之前学习过Provide和Inject,

Composition API也可以替代之前的 Provide 和 Inject 的选项。

我们可以通过 provide函数来提供数据,可以通过 provide 函数来定义每个 Property;

provide函数可以传入两个参数:

  • name:提供的属性名称;

  • value:提供的属性值;

App.vue

<template>
<div>AppContent: {{ name }}</div>
<button @click="name = 'kobe'">app btn</button>
<show-info></show-info>
</template>
<script>
import { provide, ref } from 'vue'
import ShowInfo from './ShowInfo.vue'
export default {
components: {
ShowInfo
},
setup() {
const name = ref("why")
// 响应式数据
provide("name", name)
provide("age", 18)
return {
name
}
}
}
</script>

ShowInfo.vue:

<template>
<div>ShowInfo: {{ name }}-{{ age }}-{{ height }} </div>
</template>
<script>
import { inject } from 'vue'
export default {
setup() {
const name = inject("name")
const age = inject("age")
// 可以自己设置一个默认值,当上层组件没传递时使用
const height = inject("height", 1.88)
return {
name,
age,
height
}
}
}
</script>

12.9 监听数据

<template>
<div>AppContent</div>
<button @click="message = '你好啊,李银河!'">修改message</button>
<button @click="info.friend.name = 'james'">修改info</button>
</template>
<script>
import { reactive, ref, watch } from 'vue'
export default {
setup() {
// 1.定义数据
const message = ref("Hello World")
const info = reactive({
name: "why",
age: 18,
friend: {
name: "kobe"
}
})
// 2.侦听数据的变化
watch(message, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
watch(info, (newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
}, {
immediate: true // 传入参数:是否立即监听。
})
// 3.监听reactive数据变化后, 获取普通对象
watch(() => ({ ...info }), (newValue, oldValue) => {
console.log(newValue, oldValue)
}, {
immediate: true,
deep: true
})
return {
message,
info
}
}
}
</script>

当侦听到某些响应式数据变化时,我们希望执行某些操作,这个时候可以使用 watchEffect。

我们来看一个案例:

首先,watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖;

其次,只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行;

<template>
<div>
<h2>当前计数: {{ counter }}</h2>
<button @click="counter++">+1</button>
<button @click="name = 'kobe'">修改name</button>
</div>
</template>
<script>
import { watchEffect, watch, ref } from 'vue'
export default {
setup() {
const counter = ref(0)
const name = ref("why")
// watch(counter, (newValue, oldValue) => {})
// 1.watchEffect传入的函数默认会直接被执行
// 2.在执行的过程中, 会自动的收集依赖(依赖哪些响应式的数据)
const stopWatch = watchEffect(() => {
console.log("-------", counter.value, name.value)
// 判断counter.value > 10
if (counter.value >= 10) {
stopWatch()
}
})
return {
counter,
name
}
}
}
</script>

案例:计数功能

效果图:

基于CompositionAPI,在Home与About两个组件中,分别实现计数功能。

App.vue

<template>
<home-com></home-com>
<hr>
<about-com></about-com>
</template>
<script>
import HomeCom from './views/HomeCom.vue'
import AboutCom from './views/AboutCom.vue'
export default {
name: 'App',
components: {
HomeCom,
AboutCom
}
}
</script>

HomeCom.vue

<template>
<h1>Home</h1>
<h2>计数功能:{{ counter }}</h2>
<button @click="plus">+1</button>
<button @click="minus">-1</button>
</template>
<script>
import Counter from '@/hooks/UseCounter'
export default {
name: "HomeCom",
setup() {
const {counter,plus,minus} = Counter()
return {
counter,
plus,
minus
}
}
}
</script>

AboutCom.vue

<template>
<h1>About</h1>
<h2>about的计数功能:{{ counter }}</h2>
<button @click="plus">+1</button>
<button @click="minus">-1</button>
</template>
<script>
import useCounter from "@/hooks/UseCounter";
export default {
name: "AboutCom",
setup() {
return {
...useCounter()
}
}
}
</script>
<style scoped>
</style>

UseCounter.js

import {ref} from "vue";
export default function (){
const counter = ref(0);
const plus = ()=>{
counter.value ++
}
const minus = ()=>{
counter.value --
}
return {
counter,
plus,
minus
}
}

案例:修改标题

效果图

App.vue

<template>
<h1>点击按钮,修改对应的标题</h1>
<button @click="popClick">流行音乐</button>
<button @click="softClick">轻音乐</button>
<button @click="rockClick">摇滚音乐</button>
</template>
<script>
import changeTitle from "@/hooks/changeTitle";
export default {
name: 'App',
setup() {
const title = changeTitle('修改标题')
function popClick() {
title.value = '流行音乐'
}
function softClick() {
title.value = '轻音乐'
}
function rockClick() {
title.value = '摇滚音乐'
}
return {
popClick,
softClick,
rockClick
}
}
}
</script>

hooks/changeTitle.js

import {ref, watch} from "vue";
export default function changeTitle(titleValue) {
// 定义ref的引入数据,实现响应式
const titleRef = ref(titleValue)
// 监听title的变化
watch(titleRef, (newValue) => {
document.title = newValue
}, {
immediate: true
})
// 返回ref值
return titleRef
}

13. setup函数的语法糖

上面的写法,基本上所有的逻辑代码都书写在setup函数中,这样代码的层级显得很多。

script标签中输出对象,对象中书写setup函数,而setup函数中也会引入函数、定义函数......

Vue3提供了更简介的写法:

<script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖,当同时使用 SFC 与组合式 API 时推荐该语法。

  • 更少的样板内容,更简洁的代码;

  • 能够使用纯 Typescript 声明 prop 和抛出事件;

  • 更好的运行时性能 ;

  • 更好的 IDE 类型推断性能 ;

<script setup> 里面的代码会被编译成组件 setup() 函数的内容。

这意味着与普通的 <script> 只在组件被首次引入的时候执行一次不同,<script setup> 中的代码会在每次组件实例被创建的时候执行。

App.vue:

<template>
<div>AppContent: {{ message }}</div>
<button @click="changeMessage">修改message</button>
<show-info name="why"
:age="18"
@info-btn-click="infoBtnClick"
ref="showInfoRef">
</show-info>
<show-info></show-info>
<show-info></show-info>
</template>
<script setup>
// 1.所有编写在顶层中的代码, 默认都会暴露给template可以使用
import { ref, onMounted } from 'vue'
import ShowInfo from './ShowInfo.vue'
// 2.定义响应式数据
const message = ref("Hello World")
console.log(message.value)
// 3.定义绑定的函数
function changeMessage() {
message.value = "你好啊, 李银河!"
}
function infoBtnClick(payload) {
console.log("监听到showInfo内部的点击:", payload)
}
// 4.获取组件实例
const showInfoRef = ref()
onMounted(() => {
showInfoRef.value.foo()
})
</script>

ShowInfo.vue

<template>
<div>ShowInfo: {{ name }}-{{ age }}</div>
<button @click="showInfoBtnClick">showInfoButton</button>
</template>
<script setup>
// 定义props
const props = defineProps({
name: {
type: String,
default: "默认值"
},
age: {
type: Number,
default: 0
}
})
// 绑定函数, 并且发出事件
const emits = defineEmits(["infoBtnClick"])
function showInfoBtnClick() {
emits("infoBtnClick", "showInfo内部发生了点击")
}
// 定义foo的函数
function foo() {
console.log("foo function")
}
defineExpose({
foo
})
</script>

对于组件的导入,不用再在components中注册了,导入后直接使用即可;

对于 props 和 emits 选项的生命,使用 defineProps 和 defineEmits API,它们将自动地在 <script setup> 中可用;

需要注意的是,使用 <script setup> 的组件是默认关闭的,通过模板 ref 或者 $parent 链获取到的组件的公开实例,不会暴露给任何在 <script setup> 中声明的绑定;如果需要访问组件的实例,需要用到 defineExpose,指定在 <script setup> 组件中要暴露出去的 property。

posted @   子不语2015831  阅读(122)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示