0%

Vue3入门

简介

Vue.js(简称Vue)是一款用于构建用户界面的渐进式JavaScript框架,本文我们就简单的学习一下Vue的使用。

知识图谱概览

我们学习的时候,先从总体上对事务有一个认识还是非常重要的,我从网上找了一张图片,从整体上介绍了一下学习的内容;我们的文章也会按照这个结构来编写

前端框架-vue3

构建工程项目

安装Node.js

我们可以去官网下载长期支持版本(LTS版本),点击安装即可;安装后再使用命令行查看是否安装成功

1
2
3
# 查看是否安装了node.js
node -v
npm -v

基于 Vite 创建一个Vue项目

vite是什么?问一问deepseek

1
Vite 是一款由 Vue.js 创始人尤雨溪(Evan You)开发的现代前端构建工具,旨在通过原生 ES 模块(ESM)和按需编译等特性,显著提升开发效率和构建速度。

我们打开命令行,进入希望创建工程的目录,输入命令

1
npm create vite@latest

然后按照提示输入项目名称、选择框架(选vue)和语言(选JavaScript

image-20241109230635085

很快vite就能帮我们创建好项目,接下来我们用命令行进入项目文件夹,安装依赖,再运行项目:

1
2
3
4
5
cd your-project-name
# 安装依赖包
npm install
# 运行开发环境
npm run dev

这样,我们就利用vite构建并运行了一个vue项目

image-20241109230927204

image-20241109230938683

Vue的目录结构和文件结构

目录结构

良好的目录结构可以帮助我们阅读代码,管理代码,vue项目的默认的结构比较简单,但并不能满足工程化开发,以下是推荐的项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
my-vue-project/
├── public/ # 放静态资源
├── src/
│ ├── api/ # API定义和公共方法相关
│ ├── assets/ # 图片、样式等资源
│ ├── components/ # 公共组件
│ ├── views/ # 页面视图
│ ├── router/ # 路由管理
│ ├── store/ # 状态管理
│ ├── utils/ # 一些工具类
│ └── App.vue # 根组件
├── .gitignore # Git 忽略文件配置
├── index.html # 项目入口 HTML 文件
├── package.json # 项目信息及依赖管理,运行和打包脚本
├── package-lock.json # 是npm生成的自动锁定文件,确保每次安装的依赖版本一致,不需要管。
└── vite.config.js # Vite 配置文件,配置路径别名、代理服务器、环境变量
文件结构

Vue框架中组件文件的后缀名为‘.vue’,一个vue文件就是一个组件,vue文件的结构如下:

1
2
3
4
5
6
7
8
<template>    
</template>

<script setup>
</script>

<style scoped>
</style>
  • template:负责 HTML 结构的部分,所有展示的内容都在这里定义。
  • script setup:逻辑的“后台”,数据、方法和生命周期钩子全在这里,JavaScript的代码就编写在这个标签下。
  • style scoped:用CSS定义组件样式,加上scoped后样式只会在这个组件内生效,不会影响别的地方。

语法教程

响应式

响应式系统是Vue核心特性之一,它通过数据劫持和依赖追踪实现数据与视图的自动同步。简单的说,现在我们可以很简单的同步html和JavaScript中的数据。

插值表达式

Vue.js 中的插值表达式是一种用于在 HTML 模板中动态绑定数据的语法,通过双大括号 {{ }} 包裹 JavaScript 表达式实现数据渲染,将数据属性(如 messageuser.name)实时显示在页面上,例如:

1
2
3
4
5
<template>
<h3>1. 响应式</h3>
<h4>1.1 插值表达式取值 ref和reactive函数</h4>
<p>姓名:{{ webName }}</p>
</template>

我们在模块‘template’中添加了‘’,意味着我们要将名为‘webName’的JavaScript 属性渲染到页面

响应式核心函数 - ref()和reactive()

如果我们调用后端取得键值对{“webName”: “张三”},如何将值’张三’显示到页面上呢?我们可以这样写:

1
2
3
4
5
6
7
8
9
<template>
<h3>1. 响应式</h3>
<h4>1.1 插值表达式取值 ref和reactive函数</h4>
<p>姓名:{{ webName }}</p>
</template>

<script setup>
const webName = ref("张三");
</script>

可以看到在‘script setup’模块中我们使用了响应式函数‘ref()’,代码语句‘const webName = ref(“张三”);’,指定了要将值‘张三’赋值给属性‘webName’;让我们来看看效果:

image-20250808174257235

这样我们就使用插值表达式和响应式函数将后端的数据轻松渲染到了页面上;但是到了这里我就产生了一个疑惑,为什么必须用函数ref(),我们直接赋值不行吗?我们把代码做如下修改:

1
2
3
4
5
6
7
8
9
<template>
<h3>1. 响应式</h3>
<h4>1.1 插值表达式取值 ref和reactive函数</h4>
<p>姓名:{{ webName }}</p>
</template>

<script setup>
const webName = "张三";
</script>

再次刷新页面,页面并没有变化,果然,直接赋值也是可以渲染的,那么为什么还要使用函数ref()呢?这是因为,直接赋值时Vue无法检测到’webName‘将来的变化,因此视图不会自动更新;**ref 将数据包装为响应式对象,通过 .value 修改值时,Vue 能追踪变化并**触发视图更新。我们来举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<h3>1. 响应式</h3>
<h4>1.1 插值表达式取值 ref和reactive响应式函数</h4>
<p>姓名:{{ webName }}</p>
<button @click="addNum1">加随机数</button>
</template>

<script setup>
const webName = ref("张三");
const addNum1 = () => {
webName.value = " 张三 : " + Math.random();
};
</script>

这里我们添加一个按钮,当点击该按钮,触发改变’webName‘的value;让我们看看此时页面的变化;

image-20250809103909902

当点击按钮,后端的值发生改变,页面上的值也会随之改变;这正是响应式的核心功能。

既然效果没错,那么我们再来介绍一下这两个Vue响应式函数:ref()和reactive(),他们都能在后端数据变动时自动更新视图,改变页面显示;它们的对比如下:

特性 ref reactive
适用数据类型 基本类型 + 对象类型 仅对象类型
访问方式 需通过 .value 直接访问属性(如 obj.key
模板自动解包 模板中自动解包(无需 .value 直接使用属性
解构响应性 解构后仍可通过 .value 保持 直接解构会丢失响应性,需 toRefs
重新赋值 支持(通过 .value 替换) 直接替换会破坏响应性
深度响应式 对象类型时深度响应 默认深度响应

我们再来用reactive()举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<h3>1. 响应式</h3>
<h4>1.1 插值表达式取值 ref和reactive响应式函数</h4>
<p>姓名:{{ webName }}</p>
<button @click="addNum1">加随机数</button>
<p>姓名:{{ webinfo.name }}</p>
<p>作者:{{ webinfo.age }}</p>
<button @click="addNum2">加随机数</button>
</template>

<script setup>
const webName = ref("张三");
const webinfo = reactive({
name: "qiuli",
age: "18"
});
const addNum1 = () => {
webName.value = " 张三 : " + Math.random();
};
const addNum2 = () => {
webinfo.name = "qiuli : " + Math.random();
webinfo.age = "age : " + age + Math.random();
}
</script>

<style scoped>
</style>

看看效果:

image-20250808175410675

image-20250808200056115

可以看到我们添加了一个对象‘webinfo’,对象中包含属性‘name’和‘age’,并且用插值表达式和reactive()将其渲染到页面;

我们添加两个按钮,当点击这两个按钮,就会为‘webName’和‘webinfo’中的属性添加随机数。

计算属性 - computed()

当一个数据依赖于另一个数据时(即A数据的变化依赖于B数据的变化),可以考虑用computed()。它能把计算逻辑集中在一起,提高代码的可读性,并且只有当依赖的数据发生变化时才重新计算。比如说,我们有这样一个包含嵌套数组的对象:

1
2
3
4
5
6
7
8
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})

我们想根据 author 是否已有一些书籍来展示不同的信息:

1
2
<p>Has published books:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>

这里”是否发表图书”依赖于author.books,如果这个list发生变化,我们要如何让”是否发表图书”也随之改变呢?computed()函数为我们提供了方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>

<script setup>
import { reactive, computed } from 'vue'

const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})

// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>

让我们来看看效果:

image-20250809115701196

如果我们修改author.books为空

1
2
3
4
const author = reactive({
name: 'John Doe',
books: []
})

那么效果为:

image-20250809115836828

侦听器 - watch()

这里我第一时间产生了疑问,既然前面学习的函数已经能追踪数据的变动,为什么还要有专门的侦听器呢?答案是当需要在数据变化时执行一些特定逻辑,比如发起请求、写入日志,就可以用watch()来实现。我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>    
<h4>1.3 侦听器 watch</h4>
<p>{{ num }}</p>
<button @click="addNum3">加随机数</button>
</template>

<script setup>
const num = ref(0)
const addNum3 = () => {
num.value = Math.random();
}
watch(num, (newValue, oldValue) => {
let msg = `num发生了变化, 变化前:【${oldValue}】,变化后:【${newValue}】`;
alert(msg);
})
</script>

当点击按钮,num的值发生改变,我们利用侦听器打印一条记录,效果如下:

image-20250809153311333

生命周期

生命周期图示

所谓的生命周期函数就是组件实例从创建到销毁过程中不同时间点自动调用的函数。vue基本上在每个处理节点,均提供了钩子方便我们使用。我们来看看官方图示:

组件生命周期图示

最常用函数 - onMounted()

onMounted()的作用是注册一个回调函数,在组件挂载完成后执行,粗暴点理解就是,当页面“加载完成后”,就会调用该方法。官方的格式如下:

1
function onMounted(callback: () => void): void

我们来看一个例子,当页面加载完成后,自动调用后端接口,获取文章列表,并将文章数据渲染到页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<template>    
<h3>2. 生命周期函数</h3>
<h4>onMounted()-最常用</h4>
<p>标题:{{ articleList[0].title }}}</p>
<p>摘要:{{ articleList[0].summary }}</p>
<p>作者:{{ articleList[0].author }}</p>
<p>阅读量:{{ articleList[0].readCt }}</p>
</template>

<script setup>
// 模拟后端数据
const articleList = ref([
{
"id": "100",
"title": "文章1",
"author": "qiuli",
"content": "content",
"summary": "这是文章1的摘要",
"readCt": 100,
"imgUrl": "/src/assets/article.png"
}
])

onMounted(() => {
// 模拟异步API请求数据
setTimeout(() => {
articleList.value = [];
articleList.value.push({
title: "一小时构建Vue知识体系-async",
summary: "一小时构建Vue知识体系-async",
author: "qiuli",
readCt: 19,
imgUrl: "/src/assets/article.png"
})
}, 2000)
})
</script>

让我们来看看效果:

image-20250809162930977

内置指令

条件渲染

条件渲染就是满足某个条件时,才进行渲染或展示,我们直接给出代码示例和效果演示

v-ifv-else

设置变量isLogin为false,利用v-if、v-else对isLogin进行判断,条件为真即渲染该代码;另设置按钮’登录‘,点击该按钮设置变量isLogin为true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>    
<h3>3. 内置指令</h3>
<h4>3.1 v-if v-else</h4>
<p v-if="!isLogin">请登录</p>
<p v-else>登录成功</p>
<button @click="doLogin">登录</button>
</template>

<script setup>
const isLogin = ref(false)
const doLogin = () => {
isLogin.value = true;
}
</script>

效果演示

screen-1754730410621

v-show

设置变量isvshowLogin为false,利用v-show对isvshowLogin进行判断,条件为真即渲染该代码;另设置按钮’登录‘,点击该按钮设置变量isvshowLogin为true

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>    
<h4>3.2 v-show</h4>
<p v-show="!isvshowLogin">请登录</p>
<p v-show="isvshowLogin">登录成功</p>
<button @click="doVshowLogin">登录</button>
</template>

<script setup>
const isvshowLogin = ref(false)
const doVshowLogin = () => {
isvshowLogin.value = true;
}
</script>

效果和v-if是一样的,这里不再截图

v-ifv-show的区别

既然这两个指令的均能实现类似的效果,为什么vue还要提供两个不同的指令呢?这里我们直接看官方的解释:

  • v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。
  • v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。
  • 相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换。

总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。

列表渲染
v-for

简介

v-for 指令基于一个数组来渲染一个列表。v-for 指令的值需要使用 item in items 形式的特殊语法,其中 items 是源数据的数组,而 item 是迭代项的别名。v-for 也支持使用可选的第二个参数表示当前项的位置索引。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>    
<h4>3.3 v-for</h4>
<button @click="addArticle">添加文章</button>
<div v-for="(article, index) in articleList">
<h5>标题:{{ article.title }}</h5>
<p>序号:{{ index + 1 }}</p>
<p>摘要:{{ article.summary }}</p>
<p>作者:{{ article.author }}</p>
<p>阅读量:{{ article.readCt }}</p>
</div>
<h5>文章总数:{{ articleList.length }}</h5>
</template>

<script setup>
// 模拟后端数据
const articleList = ref([
{
"id": "100",
"title": "文章1",
"author": "qiuli",
"content": "content",
"summary": "这是文章1的摘要",
"readCt": 100,
"imgUrl": "/src/assets/article.png"
}
])

const addArticle = () => {
articleList.value.push(
{
title: "文章" + articleList.value.length,
summary: "这是文章" + articleList.value.length + "的摘要",
author: "qiuli",
readCt: 100,
imgUrl: "/src/assets/article.png"
}
)
}
</script>

效果演示

screen-1754731780076

可以看到,随着点击”添加文章”按钮,不断向列表中添加文章对象,v-for渲染出的文章列表也不断增加

v-forv-ifv-show的优先级问题

先说结论:v-ifv-for 的优先级高,v-showv-for 的优先级低,这意味着 v-if 的条件将无法访问到 v-for 作用域内定义的变量别名,但是v-show可以:

1
2
3
4
5
6
7
<!--
这会抛出一个错误,因为属性 todo 此时
没有在该实例上定义
-->
<div v-for="(article,index) in articleList" v-if="article.readCt > 12">
<!-- 不会报错 -->
<div v-for="(article,index) in articleList" v-show="article.readCt > 12">
监听事件

我们可以使用 v-on 指令 (简写为 @) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="handler"@click="handler"

方法事件处理器

v-on 可以接受一个方法名或对某个方法的调用;我们前面的代码

1
<button @click="addArticle">添加文章</button>

点击按钮后触发方法’addArticle‘,实现向列表中添加文章对象;

1
2
3
4
5
6
7
8
9
10
11
const addArticle = () => {
articleList.value.push(
{
title: "文章" + articleList.value.length,
summary: "这是文章" + articleList.value.length + "的摘要",
author: "qiuli",
readCt: 100,
imgUrl: "/src/assets/article.png"
}
)
}

这就是方法事件处理器,不再赘述

按键修饰符

在监听键盘事件时,我们经常需要检查特定的按键。Vue 允许在 v-on@ 监听按键事件时添加按键修饰符。按键修饰符主要用于自定义快捷键,比如说vue官网的ctr+k快速搜索就是基于该机制实现的。

语法结构为:@按键行为.按键名称,比如说@keydown.enter,当enter键被按下时触发。

  • 按键修饰符有两个按键行为事件可选,一个是按下按键时触发keydown ,一个是松开按键时触发 keyup;

  • 按键名称:Vue 为一些常用的按键提供了别名,其他26个按键以名称为准:

    • .enter
    • .tab
    • .delete (捕获“Delete”和“Backspace”两个按键)
    • .esc
    • .space
    • .up
    • .down
    • .left
    • .right

    系统按键修饰符:

    • .ctrl
    • .alt
    • .shift
    • .meta

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>    
<h4>3.4 v-on</h4>
<div v-for="(article, index) in articleList">
<h5>标题:{{ article.title }}</h5>
<p>序号:{{ index + 1 }}</p>
<p>摘要:{{ article.summary }}</p>
<p>作者:{{ article.author }}</p>
<p>阅读量:{{ article.readCt }}</p>
</div>

<input type="text" placeholder="按下ctrl+a键 添加文章" @keydown.ctrl.a="addArticle"></input>
<input type="text" placeholder="松开a键 添加文章" @keyup.a="addArticle"></input>
</template>

<script setup>
// 模拟后端数据
const articleList = ref([
{
"id": "100",
"title": "文章1",
"author": "qiuli",
"content": "content",
"summary": "这是文章1的摘要",
"readCt": 100,
"imgUrl": "/src/assets/article.png"
}
])

const addArticle = () => {
articleList.value.push(
{
title: "文章" + articleList.value.length,
summary: "这是文章" + articleList.value.length + "的摘要",
author: "qiuli",
readCt: 100,
imgUrl: "/src/assets/article.png"
}
)
}
</script>

效果演示

screen-1754742827575

可以看到我们设置了两个键盘快捷键:1. 当按下’ctrl+a‘时触发添加文章;2. 当松开按下的’a‘时触发添加文章;要注意的是现在的快捷键是有生效范围的,默认情况下,仅当绑定快捷键的元素获得焦点时,键盘事件才能被正确的触发。

若需要在当前页面全局实现快捷键,需要做一些额外的实现。这里演示基于 window.addEventListener 全局监听快捷键的方式实现一个demo(若全局快捷键比较多,可以考虑使用 Vue 插件 vue-shortkey):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
<div v-for="(article, index) in articleList">
<h5>标题:{{ article.title }}</h5>
<p>序号:{{ index + 1 }}</p>
<p>摘要:{{ article.summary }}</p>
<p>作者:{{ article.author }}</p>
<p>阅读量:{{ article.readCt }}</p>
</div>
</template>

<script setup>
const articleList = ref([
{
"id": "100",
"title": "文章1",
"author": "qiuli",
"content": "content",
"summary": "这是文章1的摘要",
"readCt": 100,
"imgUrl": "/src/assets/article.png"
}
])

onMounted(() => {
// 全局快捷键
// window.addEventListener('keydown', handleKeydown)
})

const handleKeydown = (e) => {
if (e.ctrlKey && e.key === 'a') {
// 阻止默认行为,例如浏览器的快捷键
e.preventDefault();
addArticle();
}
}
</script>

效果演示

screen-1754790810333

这次不用选中元素获取焦点,直接按下快捷键’ctrl+a‘,就可以触发添加文章

动态绑定

我们可以用v-bind来为元素动态绑定属性(attribute)及样式(即当属性及样式被修改时vue能监测到,并更新页面)

绑定属性

动态的绑定一个或多个属性,也可以是组件的 prop。用于绑定 classstyle或组件的 attribute

  • 缩写:: 或者 . (当使用 .prop 修饰符),值可以省略 (当 attribute 和绑定的值同名时,需要 3.4+ 版本)
  • 示例:动态绑定图片地址
1
<img :src="article.imgUrl" alt="图片" style="width: 100px" />
1
2
3
4
5
6
7
8
9
10
<template>
<div v-for="(article, index) in articleList">
<h5>标题:{{ article.title }}</h5>
<p>序号:{{ index + 1 }}</p>
<p>摘要:{{ article.summary }}</p>
<p>作者:{{ article.author }}</p>
<p>阅读量:{{ article.readCt }}</p>
<img :src="article.imgUrl" alt="文章图片" />
</div>
</template>

image-20250810105945079

绑定HTML&CSS

这也是常用的一个功能,我们在网页上选中某个标签时,其样式往往会改变(例如变为其他颜色),使用的就是这个功能

image-20250810111643847

标签的颜色改变

我们来看看是如何实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>   
<div :class="{ activeClass: isActive }" @click="changeColor">改变颜色</div>
</template>

<script setup>
const isActive = ref(true);
const changeColor = () => {
isActive.value = !isActive.value;
}
</script>

<style scoped>
.activeClass {
color: green;
background-color: red;
padding: 10px;
}
</style>

效果演示

screen-1754796774966

绑定内联样式

我们先看看官网的介绍::style 支持绑定 JavaScript 对象值,对应的是 HTML 元素的 style 属性:

1
2
3
4
5
6
7
8
<template>  
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
</template>

<script setup>
const activeColor = ref('red')
const fontSize = ref(30)
</script>

我看到这里有个疑问,既然已经有绑定CSS的方式了,那么为什么还要通过内联的方式绑定样式呢?我在网上看到了一个应用场景:“头像绘制,比如说飞书的群头像,允许用户选择背景色和输入文字,就可以基于该机制实现。”

image-20241114222342751

直接绑定一个样式对象通常是一个好主意,这样可以使模板更加简洁:

1
2
3
4
5
6
7
8
9
10
<template>    
<div :style="styleObject"></div>
</template>

<script setup>
const styleObject = reactive({
color: 'red',
fontSize: '30px'
})
</script>
双向绑定

在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦,v-model 指令帮我们简化了这一步骤

1
<input v-model="text">

v-model 指令的作用就是在表单输入元素或组件上创建双向绑定。什么叫双向绑定呢?两个对象:响应式数据变量 和 表单组件的值,双向绑定:互相影响,更改其中一个对象的值,另一个对象的值也会变更。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>    
<h4>v-model 双向绑定</h4>
<input type="text" v-model="articleList[0].title" placeholder="edit me" />
<button @click="saveArticleTitle">保存</button>
</template>

<script setup>
const articleList = ref([
{
"id": "100",
"title": "文章1",
"author": "qiuli",
"content": "content",
"summary": "这是文章1的摘要",
"readCt": 100,
"imgUrl": "/src/assets/article.png"
}
])

const saveArticleTitle = () => {
alert("文章标题已经被修改为:" + articleList.value[0].title);
}
</script>

效果演示

screen-1754801463630

可以看到修改表单后,JavaScript中的内容也改变了

自定义指令
示例

除了 Vue 内置的一系列指令 (比如 v-modelv-show) 之外,Vue 还允许你注册自定义的指令 (Custom Directives)。自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。一个指令的定义对象可以提供几种钩子函数 (都是可选的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode) {}
}

我们看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
// 在模板中启用 v-highlight
const vHighlight = {
mounted: (el) => {
el.classList.add('is-highlight')
}
}
</script>

<template>
<p v-highlight>This sentence is important!</p>
</template>

效果演示

image-20250810141826643

钩子参数

指令的钩子会传递以下几种参数:

  • el:指令绑定到的元素。这可以用于直接操作 DOM。

  • binding

    :一个对象,包含以下属性。

    • value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2
    • oldValue:之前的值,仅在 beforeUpdateupdated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。
  • vnode:代表绑定元素的底层 VNode。

  • prevVnode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdateupdated 钩子中可用。

懒加载图片的示例

我们来看看大佬是怎么用这个功能的

  • ImgLazyLoad.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
export default {
mounted(el, binding) {
const options = {
root: null, // 视口作为根
rootMargin: '0px', // 不设置外边距
threshold: 0.1, // 元素进入视口 10% 时触发
};

// 设置占位符图片(避免在网络加载完成前显示空白)
el.setAttribute(
'src',
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
);

const loadImage = () => {
const imageSrc = binding.value; // 获取真实图片地址
if (imageSrc) {
el.src = imageSrc; // 设置真实图片地址
el.dataset.loaded = 'true'; // 标记为已加载
observer.unobserve(el); // 停止观察
}
};

const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
loadImage(); // 进入视口时加载真实图片
}
});
}, options);

observer.observe(el); // 开始观察元素
},
};
  • main.js 中注册 v-lazy-load指令:
1
2
3
4
5
6
7
import { createApp } from 'vue'
import App from './App.vue'
import lazyLoadDirective from './components/directives/ImgLazyLoad.js';

createApp(App)
.directive('lazy-load', lazyLoadDirective)
.mount('#app')
  • App.vue使用自定义指令:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
<img
v-for="(image, index) in images"
:key="index"
v-lazy-load="image.src"
alt="Blog Image"
class="lazy-image"
/>
</template>

<script setup>
import {ref} from 'vue';

const images = ref([
{src: 'xxx'},
{src: 'xxx'},
{src: 'xxx'},
{src: 'xxx'},
{src: 'xxx'},
{src: 'xxx'},
{src: 'xxx'},
{src: 'xxx'},
])
</script>

<style>
.lazy-image {
width: 100%;
height: auto;
opacity: 0; /* 初始透明度为0,隐藏图片以便在加载后显示 */
transition: opacity 0.5s ease-in-out; /* 添加过渡效果,图片透明度变化会在 0.5秒内平滑进行 */
}

/* 定义当图片加载完成后应用的样式 */
.lazy-image[data-loaded] {
opacity: 1; /* 将透明度设置为1,显示图片 */
}
</style>

组件化

组件与组件关系

什么是组件?简单来说,组件就是一段封装好的代码,负责一部分独立的功能。组件可以被别的组件引用,父组件可以引用子组件,并且使用子组件的HTML&CSS页面和JavaScript方法。我们来看一个简单的例子

  • GrandSonDemo.vue
1
2
3
4
5
6
7
<template>
<h3>这是孙组件</h3>
</template>

<script setup></script>

<style scoped></style>
  • SonDemo.vue
1
2
3
4
5
6
7
8
9
10
<template>
<h3>这是子组件</h3>
<grand-son-demo />
</template>

<script setup>
import GrandSonDemo from "./GrandSonDemo.vue";
</script>

<style scoped></style>
  • FatherDemo.vue
1
2
3
4
5
6
7
8
9
10
<template>
<h3>这是父组件</h3>
<SonDemo />
</template>

<script setup>
import SonDemo from "./SonDemo.vue";
</script>

<style scoped></style>

效果演示

image-20250810155941058

插槽 - slot

插槽是组件间内容分发的“百宝箱”。你可以把插槽理解为子组件给父组件留的一块空地,父组件可以随时填充内容。

注意填充的内容:可以是纯文本,也可以是html代码块(也成为模板内容)。

默认插槽
  • <slot> 元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
  • <slot> 元素内的内容,在外部没有提供任何内容的情况下,就是该插槽的默认内容。
  • 注意:这里我们并没有给<slot> 元素设置一个名字,vue会给一个默认值,等价于<slot name="default">

我们来看代码

  • SonDemo.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<h3>这是子组件</h3>
<grand-son-demo />

<div class="slot_demo_class">
<slot>
<p>这是默认插槽</p>
</slot>
</div>
</template>

<script setup>
import GrandSonDemo from "./GrandSonDemo.vue";
</script>

<style scoped>
.slot_demo_class {
color: red;
}
</style>

效果演示

image-20250810163837600

我们在SonDemo.vue中添加slot元素,此时没有父组件为插槽提供内容,所以插槽显示的就是其元素内的内容;现在我们修改Father.vue的代码,为插槽提供内容

  • Father.vue
1
2
3
4
5
6
7
8
9
10
11
12
<template>
<h3>这是父组件</h3>
<SonDemo>
为默认插槽赋值
</SonDemo>
</template>

<script setup>
import SonDemo from "./SonDemo.vue";
</script>

<style scoped></style>

效果演示

image-20250810164505610

可以看到,子组件中的插槽接收了父组件中传递的内容

具名插槽

顾名思义,具名插槽就是有指定名称的插槽。使用场景是,在同一个组件中提供多个插槽,需要有名称做区分;

我们修改SonDemo.vue,添加两个具名插槽

  • SonDemo.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<h3>这是子组件</h3>
<grand-son-demo />

<div class="slot_demo_class">
<slot>
<p>这是默认插槽</p>
</slot>
<br/>
<slot name="header_slot" class="slot_demo_class">
<p>这是具名插槽,名称为header_slot</p>
</slot>
<br/>
<slot name="footer_slot" class="slot_demo_class">
<p>这是具名插槽,名称为footer_slot</p>
</slot>
</div>
</template>

<script setup>
import GrandSonDemo from "./GrandSonDemo.vue";
</script>

<style scoped>
.slot_demo_class {
color: red;
}
</style>

再修改Father.vue的代码,为具名插槽赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<h1>这是父组件</h1>

<SonDemo>
为默认插槽赋值

<template #header_slot>为头部插槽赋值</template>

<template v-slot:footer_slot>为底部插槽赋值</template>
</SonDemo>
</template>


<script setup>
import SonDemo from "./SonDemo.vue";
</script>

<style scoped></style>

效果演示

image-20250810170205026

作用域插槽

在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据,这时我们就需要把子组件中的数据传递给父组件

  • SonDemo.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<h3>这是子组件</h3>
<grand-son-demo />

<div class="slot_demo_class">
<slot>
<p>这是默认插槽</p>
</slot>
<br/>
<slot name="header_slot" class="slot_demo_class">
<p>这是具名插槽,名称为header_slot</p>
</slot>
<br/>
<slot name="footer_slot" class="slot_demo_class">
<p>这是具名插槽,名称为footer_slot</p>
</slot>
<br/>
<slot name="temp_slogan_slot" class="slot_demo_class" :tempInt="123">
<p>这是作用域插槽,可以为父组件传递参数</p>
</slot>
</div>
</template>

<script setup>
import GrandSonDemo from "./GrandSonDemo.vue";
</script>

<style scoped>
.slot_demo_class {
color: red;
}
</style>
  • Father.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<h1>这是父组件</h1>

<SonDemo>
为默认插槽赋值

<template #header_slot>为头部插槽赋值</template>

<template v-slot:footer_slot>为底部插槽赋值</template>

<template v-slot:temp_slogan_slot="scope">
这是作用域插槽,传递的参数为:{{scope.tempInt}}
</template>
</SonDemo>
</template>


<script setup>
import SonDemo from "./SonDemo.vue";
</script>

<style scoped></style>

效果演示

image-20250810171503973

父组件传递信息给子组件的通信工具 - defineProps

父组件要将大量信息传递给子组件(对象、列表等),可以在子组件中使用 defineProps接收父组件向子组件传递的属性(props)。例如我们的页面由多篇文章构成,父组件要将文章内容传递给子组件,我们先创建文章详情页的组件BaseArticle.vue,并添加代码

  • BaseArticle.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>
<h5>标题:{{ articleProps.title }}</h5>
<h6>ID:{{ articleProps.id }}</h6>
<p>作者:{{ articleProps.author }}</p>
<p>内容:{{ articleProps.content }}</p>
<p>阅读量:{{ readCount }}</p>
</template>

<script setup>
import { ref } from 'vue';

const articleProps = defineProps({
// 定义id字段,且约定必须传,类型为Number
id: {
type: Number,
required: true
},
// 定义title字段,且约定必须传,类型为String,默认值为'default title'
title: {
type: String,
required: true,
default: 'default title'
},
// 定义author字段,且约定必须传,类型为String
author: {
type: String,
required: true
},
// 定义content字段,不做任何约束,类型为任意类型
content: {}
});
</script>

<style scoped></style>

我们在子组件中定义接收的对象articleProps,再修改父组件的代码

  • Father.vue
1
2
3
4
5
6
7
8
9
10
11
12
<template>    
<BaseArticle :id="article.id" :title="article.title" :author="article.author" :content="article.content" />
</template>

<script setup>
const article = ref({
"id": "100",
"title": "文章1",
"author": "qiuli",
"content": "content"
})
</script>

在父组件中,我们将对象信息通过通过属性绑定的方式传递给子组件

效果演示

image-20250810174214575

父组件调用子组件方法的工具 - defineExpose

有时,父组件需要调用子组件的方法,这就要用 refdefineExpose

我们修改子组件的代码,在其中暴露给父组件调用的方法

  • BaseArticle.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<p>阅读量:{{ readCount }}</p>
</template>

<script setup>
import { ref } from 'vue';

// 初始化变量
const readCount = ref(0);

// 定义暴露给父组件的方法
defineExpose({
addReadCount() {
readCount.value++;
alert("阅读量增加");
}
});
</script>

再修改父组件代码,调用该方法

  • Father.vue
1
2
3
4
5
6
7
8
9
10
11
<template>    
<BaseArticle ref="readCountRef"></BaseArticle>
<button @click="callSonMethod">调用子组件的方法</button>
</template>

<script setup>
const readCountRef = ref(null);
const callSonMethod = () => {
readCountRef.value.addReadCount();
}
</script>

效果演示

screen-1754819369608

子组件传递信息给父组件的工具 - emits与defineEmits

如果需要子组件向父组件发送消息,可以在子组中使用defineEmits 或者$emit

  • 在子组件SonEmitsDemo.vue中申明事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<!-- 子组件 使用 `$emit` 方法触发自定义事件-->
<button @click="$emit('sonEmitEvent','这是子组件$emit传递的参数')">这是子组件的$emit按钮</button>
<br>
<br>
<button @click="buttonClick('这是子组件defineEmits传递的参数')">这是子组件的defineEmits按钮</button>
</template>

<script setup>
// 子组件可以显式地通过 defineEmits() 宏来声明它要触发的事件
const emit = defineEmits(['sonDefineEmitsEvent', 'otherEvent'])

function buttonClick(msg) {
emit('sonDefineEmitsEvent',msg)
}
</script>
  • 父组件中通过v-on+事件名,监听:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<!-- 父组件通过`v-on`监听子组件的事件,不管子组件是通过`$emit`还是`defineEmits`触发的事件,父组件都可以监听到.-->
<SonEmitsDemo @sonEmitEvent="handleSonEmitsEvent"
@sonDefineEmitsEvent="handleSonDefineEmitsEvent"/>
</template>

<script setup>
import SonEmitsDemo from "./components/SonEmitsDemo.vue";

const handleSonEmitsEvent = (msg) => {
alert(`Received event from son: ${msg}`)
}
const handleSonDefineEmitsEvent = (msg) => {
alert(`Received event from son: ${msg}`)
}
</script>

效果演示

image-20241117204059357

祖宗组件向后代通信 - provide与inject

一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。有两个关键的参数:

1
2
- provide :父组件提供参数;
- inject:其他后代组件获取参数;
  • FatherDemo.vue
1
2
3
4
5
6
7
8
<template>    
<SonDemo />
</template>

<script setup>
import SonDemo from "./SonDemo.vue";
provide('msgKey', '这是提供的 top组 件的 provide demo 数据')
</script>
  • SonDemo.vue
1
2
3
4
5
6
7
8
9
<template>
<h1>这是SonDemo组件</h1>
<h2>SonDemo 监听消息:{{ message }}</h2>
<GrandSonDemo />
</template>

<script setup>
const message = inject('msgKey');
</script>
  • GrandSonDemo.vue
1
2
3
4
5
6
7
8
<template>
<h1>这是GrandSonDemo组件</h1>
<h2>GrandSonDemo 监听消息:{{ message }}</h2>
</template>

<script setup>
const message = inject('msgKey');
</script>

效果演示

image-20250810201408048