Vue
基础语法
mustache 语法
<h2>{{ one + '' + two }}</h2>
<h2>{{ one }} {{ two }}</h2>
<h2>{{ three * 2 }}</h2>
data() {
return {
one: '一',
two: '二',
three: 3
}
}
通过{{}}
,template里既可以使用**data()**里面定义的数据啦
低频指令
- 本质就是自定义属性
这些指令很少使用~
v-cloak
防止页面加载时出现闪烁问题
<div v-cloak>{{ msg }}</div>
data() {
return {
msg: 'hello'
}
}
[v-cloak] {
display: none;
}
v-text
- 用于将数据填充到标签中,作用与插值表达式类似,但是没有闪动问题
- 如果数据中有 HTML 标签会将 html 标签一并输出
- 注意:此处为单向数据绑定,数据对象上的值改变,插值会发生变化;但是当插值发生变化并不会影响数据对象的值
<p v-text="msg"></p>
data() {
return {
msg: 'hello'
}
}
v-html
- 用法和 v-text 相似 但是他可以将 HTML 片段填充到标签中
- 可能有安全问题, 一般只在可信任内容上使用
v-html
,永不用在用户提交的内容上 - 它与 v-text 区别:v-text输出的是纯文本,浏览器不会对其再进行 html 解析,但 v-html 会将其当 html 标签解析后输出。
<p v-html="html"></p>
<p v-text="text"></p>
<p>{{ message }}</p>
data() {
return {
message: '<span>通过双括号绑定</span>',
html: '<span>html标签在渲染的时候被解析</span>',
text: '<span>html标签在渲染的时候被源码输出</span>',
}
}
v-pre
- 显示原始信息跳过编译过程
- 跳过这个元素和它的子元素的编译过程。
- 一些静态的内容不需要编译加这个指令可以加快渲染
<span v-pre>{{ this will not be compiled }}</span> <span v-pre>{{ msg }}</span>
data() {
return {
msg: 'Hello'
}
}
v-once
- 执行一次性的插值(当数据改变时,插值处的内容不会继续更新)
<span v-once>{{ msg }}</span>
data() {
return {
msg: 'Hello'
}
}
插值表达式 v-text v-html 三者区别
差值表达式 | 闪动问题---v-cloak 解决 |
---|---|
v-text | 没有闪动问题 |
v-html | 安全问题 |
自定义指令
除了 v-for、v-show、v-model 等等指令,vue 也允许我们自定 义指令
在某些情况下,需要对元素进行 DOM 操作,这时就需要用到自定义指令了
通过directives 选项(局部,只能在当前组件使用;全局的要使用 app 的directive())
案例-某个元素挂载完成后自动获取焦点
<input type="text" ref="input" />
import { ref } from 'vue'
setup() {
const inputRef = ref(null)
onMounted(() => {
inputRef.value.focus()
})
return {
input
}
}
这是当前输入框,要是想其它输入框也能挂载完成之后获取焦点呢?
你可能会想到复用。非常棒!
vue 中复用代码的形式主要是组件,当然还有Composition API抽取成一个hook 函数
还有一个更简单的方法:自定义指令
我们来自定义一个v-focus指令(当然,vue 中并没有这个指令哦)
<input type="text" v-focus />
directives: {
focus: {
mounted(el) {
el.focus()
}
}
}
这是局部的,当input被挂载完成后,执行input.focus()
来看看全局是怎么做的
import { createApp } from "vue";
const app = createApp(根组件);
app.directive("focus", {
mounted(el) {
el.focus();
},
});
指令的生命周期
和组件的生命周期类似都是在特定时间节点回调对应函数
自定义指令的修饰符
自定义指令的修饰符放在哪里呢?
指令生命周期函数的第二参数
<input type="text" v-focus.test="hhh" />
directives: {
focus: {
mounted(el, bindings) {
el.focus()
console.log(bindings.modifiers)
}
}
}
你就会在控制台看到 hhh
案例-转化时间戳
在开发中,大多数情况下从服务器获取到都是时间戳;
需要将时间戳转化成具体格式的时间来展示;
vue2 可以使用过滤器来完成(vue3 已移除啦);
vue3 中可以通过computed()或者自定义一个方法完成;
其实还可以通过自定义指令
可以指定一个v-format-time的指令
<h2 v-format-time>{{ timestamp }}</h2>
setup() {
const timestamp = 1623352193
return {
timestamp
}
}
来全局注册这个指令
import { createApp } from "vue";
import dayjs from "dayjs";
const app = createApp(根组件);
app.directive("format-time", {
mounted(el) {
const textContent = el.textContent;
let timestamp = parseInt(textContent);
// 如果是10位,那就是s,需转化为ms
if (textContent.length === 10) {
timestamp = timestamp * 1000;
}
el.textContent = dayjs(timestamp).format("YYYY-MM-DD HH:mm:ss");
},
});
这里使用第三方库dayjs进行转化
如果用户想自己设置格式,可以传参进来
<h2 v-format-time="YYYY/MM/DD">{{ timestamp }}</h2>
import { createApp } from "vue";
import dayjs from "dayjs";
const app = createApp(根组件);
app.directive("format-time", {
mounted(el, bindings) {
let formatString = bindings.value;
// 如果没传就用默认的
if (!formatString) {
formatString = "YYYY-MM-DD HH:mm:ss";
}
const textContent = el.textContent;
let timestamp = parseInt(textContent);
// 如果是10位,那就是s,需转化为ms
if (textContent.length === 10) {
timestamp = timestamp * 1000;
}
el.textContent = dayjs(timestamp).format(formatString);
},
});
数据绑定 v-bind
v-bind:class,语法糖-->:class
<img v-bind:src="url" alt="" /> <img :src="url" alt="" />
data() {
return {
url: 'https://pics5.baidu.com/feed/63d9f2d3572c11df69f722f63ced03d9f603c211.jpeg?token=447e2f91bfb2194ae769094c0ba5c2a5'
}
}
动态绑定 class
对象形式
<h2 :class="{ active: isActive }">你好</h2>
<button v-on:click="btnClick">点击</button>
data() {
return {
isActive: false
}
},
methods: {
btnClick: function () {
this.isActive = !this.isActive
}
}
当你点击按钮,你会发现 h2 出现一个叫 active 的类
数组形式
<h2 :class="[active, line]">你好</h2>
data() {
return {
active: 'aa',
line: 'bb'
}
}
你会发现 h2 有aa,bb这两个类名了
动态绑定 style
对象形式
<h2 :style="{ color: finalColor }">你好</h2>
data() {
return {
finalColor: 'red'
}
}
案例-点击列表项自动变红
<ul>
<li
v-for="(item, index) in list"
v-on:click="listClick(index)"
:class="{ active: current === index }"
>
{{ index }}-{{ item }}
</li>
</ul>
data() {
return {
current: 0,
list: ['海贼王', 'abs', '666']
}
},
methods: {
listClick(index) {
this.current = index
}
}
.active {
color: red;
}
计算属性 computed
当数据需要经过处理再显示时,需要用到计算属性
基本使用
<h2>{{ getFullName }}</h2>
data() {
return {
firstName: 'first',
lastName: 'last'
}
},
computed: {
getFullName() {
return this.firstName + ' ' + this.lastName
}
}
set 和 get
computed 原理是这样的
<h2>{{ fullName }}</h2>
data() {
return {
firstName: 'first',
lastName: 'last'
}
},
computed: {
// 简写
// fullName() {
// return this.firstName + ' ' + this.lastName
// }
// 完整写法
// 而一般情况下是不用set方法的,只读属性
fullName: {
set() {},
get() {
return this.firstName + ' ' + this.lastName
}
}
}
computed 和 methods 的对比--掌握
methods 和 computed 看起来都可以实现我们的功能,他们的区别在哪里?
计算属性会进行缓存,如果多次使用,计算属性只会调用一次。
<!-- methods -->
<h2>{{ getFullName() }}</h2>
<h2>{{ getFullName() }}</h2>
<h2>{{ getFullName() }}</h2>
<h2>{{ getFullName() }}</h2>
<!-- 计算属性 -->
<h2>{{ fullName }}</h2>
<h2>{{ fullName }}</h2>
<h2>{{ fullName }}</h2>
<h2>{{ fullName }}</h2>
data() {
return {
firstName: 'first',
lastName: 'last'
}
},
computed: {
fullName: function () {
// 只打印一次
console.log('fullName')
return this.firstName + ' ' + this.lastName
}
},
methods: {
getFullName: function () {
// 每打印4次
console.log('getFullName')
return this.firstName + ' ' + this.lastName
}
}
侦听器 watch
Vue 提供了一种更通用的方式来观察和响应当前活动的实例上的数据变动:侦听属性(watch)。
当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的;
默认情况下,当一个对象发生改变时可以监听到,但是对象内部成员发生改变则监听不到
data() {
return {
info: { name: 'zsf', age: '18'}
}
},
watch: {
info(newInfo, oldInfo) {
console.log('newValue:', newInfo, 'oldValue:', oldInfo )
}
},
methods: {
changeInfo() {
this.info = {name: 'kobe'}
}
changeInfoName() {
this.info.name = 'kobe'
}
}
当触发某个事件执行changeInfoName()时,改变info成员name的值,watch监听不到,所以控制台没打印。
深度侦听
但是要是有这需求呢?
这就需要深度侦听了。
方法一
data() {
return {
info: { name: 'zsf', age: '18'}
}
},
watch: {
info:{
handler: function(newInfo, oldInfo) {
console.log('newValue:', newInfo, 'oldValue:', oldInfo )
},
deep: true
}
},
methods: {
changeInfo() {
this.info = {name: 'kobe'}
}
changeInfoName() {
this.info.name = 'kobe'
}
}
方式二
watch: {
info:{
'info.name': function(newInfo, oldInfo) {
console.log('newValue:', newInfo, 'oldValue:', oldInfo )
},
deep: true
}
}
但是这种方式在 vue3 官方文档上已经看不到了。
立即执行
当如果需要页面渲染后,不管数据有没有发生改变,都要执行一次侦听器,这时就需要用到immediate属性了
watch: {
info:{
handler: function(newInfo, oldInfo) {
console.log('newValue:', newInfo, 'oldValue:', oldInfo )
},
deep: true,
immediate: true
}
}
事件监听 v-on
v-on:click,语法糖@click
参数传递
1.事件绑定的方法可以不带小括号(如果不需要参数);
2.函数定义的时候需要参数,但是事件触发时绑定的函数没有(),vue 会默认将浏览器产生的event事件对象作为参数传入方法中;
3.需要event 对象,同时又需要其它参数,这是需要手动传入$event;
<button @click="btn1Click">按钮1</button>
<button @click="btn2Click">按钮2</button>
<button @click="btn3Click(abc, $event)">按钮3</button>
methods: {
btn1Click () {
console.log(123);
},
btn2Click (name) {
console.log(name);
},
btn3Click (abc, event) {
console.log(abc, event);
}
}
修饰符
- .stop 的使用---防止事件冒泡
- .prevent 的使用---防止事件的默认行为,如表单 submit 的默认行为
- .enter 的使用---当输入回车才会触发事件(其它特殊键帽类似)
- .once 的基本使用---只触发一次回调
<div @click="divClick()">
12345
<button @click.stop="btnClick()">点击</button>
</div>
methods: {
divClick () {
console.log('div');
},
btnClick () {
console.log('btn');
}
}
双向数据绑定 v-model
原理
v-model 背后有两个操作:
- v-bind 绑定 value 属性的值;
- v-on 绑定 input 事件监听的函数中,函数会获取最新的值赋值到绑定的属性中;
<input v-model="msg" />
等价于
<input v-bind="msg" v-on="msg = $event.target.value" />
应用场景
表单
- checkbox
- radio
- select
checkbox
当多个 checkbox 用 v-model 绑定一个数据(hobbies)时(记得每个加上 value 哦),修改选中的数量的时候,hobbies 会动态的增删
<form action="">
<label for="basketball">
<input
type="checkbox"
name=""
id="basketball"
v-model="hobbies"
value="basketball"
/>篮球
</label>
<label for="football">
<input
type="checkbox"
name=""
id="football"
v-model="hobbies"
value="football"
/>足球
</label>
<label for="tennis">
<input
type="checkbox"
name=""
id="tennis"
v-model="hobbies"
value="tennis"
/>网球
</label>
</form>
radio
和多选框不同,两个radio用v-model绑定一个数据(gender),由于互斥,gender最终只能有一个value
<label for="male">
<input type="radio" id="male" v-model="gender" value="male" />男
</label>
<label for="female">
<input type="radio" id="female" v-model="gender" value="female" />女
</label>
sellect 同理
修饰符
由于 v-model 是集成了 v-bind 和 v-on,所以他也有修饰符
- lazy
- number
- trim
lazy 修饰符有什么作用呢?
默认情况下,v-model在进行双向绑定时,绑定的是input事件,每次内容输入后就将最新的值和绑定的属性同步;
如果v-model加上lazy修饰符,会将绑定的事件切换为change事件,只有在提交时(比如回车),才会触发(类似于防抖)
number 修饰符有什么作用呢?
给v-model 赋值时,不管内容是什 么类型,都会转换成String类型,但要是希望数字类型不要被转换呢?
用number修饰符。
trim
去空格。。。
条件判断 v-if
案例--登陆切换
<span v-if="isUssr">
<label for="username">用户账号</label>
<input type="text" id="uername" placeholder="用户账号" />
</span>
<span v-else>
<label for="email">用户邮箱</label>
<input type="text" id="email" placeholder="用户邮箱" />
</span>
<button @click="isUssr = !isUssr">切换类型</button>
data() {
return {
isUser: true
}
}
v-show 和 v-if 的区别
v-if 确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。;
v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块;
相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display进行切换;
注意,v-show 不支持 template;
一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销;
因此,如果需要非常频繁地切换,则使用 v-show 较好;
如果在运行时条件很少改变,则使用 v-if 较好;
遍历 v-for
<li v-for="item in list" :key="item">{{ item }}</li>
data() {
return {
list: [1,2,3,4,5]
}
}
v-for 中 key 有什么作用?
- key 属性主要用在 Vue 的虚拟 DOM 的 diff 算法,在新旧 nodes对比是辨识VNodes
- 如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法
- 而使用 key时,它会基于 key 的变化重新排列元素顺序,并且会移除/销毁key 不存在的元素
这时候你可能要问:啥是 VNode?
- 在 Vue 中,无论是组件还是元素,最终表现出来的都是一个个 VNode
- VNode 的本质是一个 js 对象
比如
<div class="my" style="font-size: 30px; color: red">hhh</div>
转化成 VNode 形式
const vnode = {
type: 'div',
prop: {
class: 'my',
style: {
font-size: 30px,
color: red
}
},
children: 'hhh'
}
所以你知道为什么v-bind
支持对象绑定了吧!
所以 Vue 的渲染过程大致理解为:template -> VNode -> 真实 DOM
使用 VNode 有一个很重要的原因:跨平台!
可以在浏览器上渲染,可以在移动端渲染
那,啥是虚拟 DOM?
VNode 组成的 VNode Tree
好了,接下来举例体现 v-for 中 key 的作用
假设一开始要遍历数组 arr = [a, b, c, d]
现在要在 bc 之间插入一个 f
当数组 变化了之后,要重新遍历了
那有个问题来了:要怎么做,才能让这个插入性能最高效?
**方案一,**把原来的数组去掉,用新的数组(ps:狗都不用)
**方案二,**ab 不变,用原来的位置,之前 c 位置换成 f,之前 d 的位置放 c,依次类推。。。要是放的位置很靠前,并且这数组巨大,那还不如用方案一呢
**方案三,**diff 算法,原来的元素不变,对比新旧 VNode 有哪些需要发生变化再变化,但是 Vue 会根据你有没有 key 采取不同的更新策略:
如图,这里引用 coderwhy 老师的一张图,当没有 key 时,它是这样更新的(具体看源码)
用方案二更新
当有 key 时,这就相对复杂了:
先从头开始,while 循环找出哪些不变;
剩下部分从尾开始,whil 循环找出哪些不变;
那剩下的是没有与之匹配的节点 f,这时直接新增一个节点
这里又引用 coderwhy 老师的一张图:
这还是比较简单的情况。。。
源码就先到这里,我只是想举出有 key 和没 key 对性能的影响
数组中响应式的方法
-
push()
this.list.push('aaa')
-
pop():删除数组最后一个
this.list.pop('aaa')
-
shift():删除数组第一个
this.list.shift()
-
unshift():在数组最前面添加
this.list.unshift()
-
splice(): 删除/插入/替换 第一个参数是开始位置
删除元素:第二个参数传入你要删除几个元素(如果没有传,就删除后面的所有元素)
替换元素:第二个参数,表示我们要替换几个元素,后面是用于替换前面的元素
插入元素:第二个参数,传入 0,并且后面跟上要插入的元素
this.list.splice(1, 3, 'a', 'b', 'c')
-
sort()
this.list.sort()
-
reverse()
this.list.reverse
图书购物车
结构样式初始化
<table>
<thead>
<tr>
<th></th>
<th>书籍名称</th>
<th>出版日期</th>
<th>价格</th>
<th>购买数量</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in books" :key="item">
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.date }}</td>
<td>{{ item.price }}</td>
<td>
<button>-</button>
{{ item.count }}
<button>+</button>
</td>
<td>移除</td>
</tr>
</tbody>
</table>
data() {
return {
books: [
{
id: 1,
name: '《算法导论》',
date: '2006-9',
price: 85.00,
count: 1
},
{
id: 2,
name: '《Linux编程艺术》',
date: '2006-2',
price: 59.00,
count: 1
},
{
id: 3,
name: '《海贼王》',
date: '2003-4',
price: 88.00,
count: 1
},
{
id: 4,
name: '《蜡笔小新》',
date: '2008-10',
price: 39.00,
count: 1
},
{
id: 5,
name: '《哈哈哈》',
date: '2007-8',
price: 24.00,
count: 1
}
]
}
}
table {
border: 1px solid #e9e9e9;
border-collapse: collapse;
border-spacing: 0;
}
th,
td {
padding: 8px 16px;
border: 1px solid #e9e9e9;
text-align: left;
}
th {
background-color: #f7f7f7;
color: #5c6b77;
font-weight: 600;
}
效果
价格格式处理
在价格前面加‘¥’;
需要保留两位小数,**toFixed()**可以把小数点后的 00 显示出来;
使用过滤器
filters: {
showPrice (price) {
return '¥' + price.toFixed(2)
}
}
价格那一栏用 item.price | showPrice
过滤器语法:会把 item.price
当成参数传进 showPrice 函数,类似 linux 的管道
<tr v-for="item in books" :key="item.id">
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.date }}</td>
<td>{{ item.price | showPrice }}</td>
<td>
<button>-</button>
{{ item.count }}
<button>+</button>
</td>
<td><button>移除</button></td>
</tr>
效果
加减按钮的事件
如何保证操作的是当前书籍?
index.html+
v-for 遍历的时候加上 index 参数
<tr v-for="(item, index) in books" :key="item.id">
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.date }}</td>
<td>{{ item.price | showPrice }}</td>
<td>
<button @click="decrement(index)">-</button>
{{ item.count }}
<button @click="increment(index)">+</button>
</td>
<td><button>移除</button></td>
</tr>
methods: {
increment (index) {
this.books[index].count++
},
decrement (index) {
this.books[index].count--
}
}
怎么做使书籍数量为 1 的时候不能减?
当书籍数量小于或等于 1 时禁用 button
<button @click="decrement(index)" :disabled="item.count <= 1">-</button>
效果
移除按钮的事件
如何确保移除的是当前书籍?
同上,传 index
index.html+
<td><button @click="remove(index)">移除</button></td>
methods: {
...
remove (index) {
this.books.splice(index, 1)
}
}
当移除完购物车,显示购物车为空怎么实现?
用 v-if 和 v-else
当 books 有长度时才显示表格
<div v-if="books.length">...</div>
<h2 v-else>购物车空啦</h2>
计算总价
用计算属性 computed + 过滤器
computed: {
totalPrice () {
let totalPrice = 0
for (let i = 0; i < this.books.length; i++) {
totalPrice += this.books[i].count * this.books[i].price
}
return totalPrice
}
}
效果
封装 TabBar 组件
初始化结构
App.vue
<div id="app">
<TabBar></TabBar>
</div>
import TabBar from "./components/tabbar/TabBar.vue";
components: {
TabBar;
}
TabBar.vue
<div id="tab-bar">
<div class="tab-bar-item">首页</div>
<div class="tab-bar-item">分类</div>
<div class="tab-bar-item">购物车</div>
<div class="tab-bar-item">我的</div>
</div>
#tab-bar {
display: flex;
position: fixed;
left: 0;
right: 0;
bottom: 0;
background-color: #f6f6f6;
box-shadow: 0 -1px 1px rgba(100, 100, 100, 0.2);
}
.tab-bar-item {
flex: 1;
text-align: center;
height: 49px;
}
TabBar 进一步抽离出 TarBarItem
新增一个 TarBarItem 组件
TarBarItem.vue
<div class="tab-bar-item">
<img src="../../assets/img/tabbar/home.svg" alt="" />
<div>首页</div>
</div>
.tab-bar-item {
flex: 1;
text-align: center;
height: 49px;
font-size: 14px;
}
.tab-bar-item img {
vertical-align: middle;
height: 24px;
width: 24px;
margin-top: 3px;
}
TabBar.vue
<div id="tab-bar">
<slot></slot>
</div>
#tab-bar {
display: flex;
position: fixed;
left: 0;
right: 0;
bottom: 0;
background-color: #f6f6f6;
box-shadow: 0 -1px 1px rgba(100, 100, 100, 0.2);
}
App.vue
<div id="app">
<TabBar>
<TabBarItem></TabBarItem>
<TabBarItem></TabBarItem>
<TabBarItem></TabBarItem>
<TabBarItem></TabBarItem>
</TabBar>
</div>
import TabBarItem from "./components/tabbar/TabBarItem.vue";
components: {
TabBarItem;
}
效果
问题:同一个图片,一样的文字
TarBarItem.vue 的内容是写死的吗?
不是,加入具名插槽
TarBarItem.vue
<div class="tab-bar-item">
<solt name="item-icon"></solt>
<solt name="item-text"></solt>
</div>
App.vue
<div id="app">
<tab-bar>
<tab-bar-item>
<img slot="item-icon" src="./assets/img/tabbar/home.svg" alt="" />
<div slot="item-text">首页</div>
</tab-bar-item>
<tab-bar-item>
<img slot="item-icon" src="./assets/img/tabbar/category.svg" alt="" />
<div slot="item-text">分类</div>
</tab-bar-item>
<tab-bar-item>
<img slot="item-icon" src="./assets/img/tabbar/shopcart.svg" alt="" />
<div slot="item-text">购物车</div>
</tab-bar-item>
<tab-bar-item>
<img slot="item-icon" src="./assets/img/tabbar/profile.svg" alt="" />
<div slot="item-text">我的</div>
</tab-bar-item>
</tab-bar>
</div>
处于激活状态的图片和文字
TabBarItem.vue
新增:一个数据 isActive、一个类 active
<div class="tab-bar-item">
<div v-if="!isActive">
<slot name="item-icon"></slot>
</div>
<div v-else>
<slot name="item-icon_active"></slot>
</div>
<div :class="{active: isActive}">
<slot name="item-text"></slot>
</div>
</div>
data () {
return {
isActive: true
}
}
.active {
color: lightcoral;
}
经验:
当插槽有属性时,为了防止使用时覆盖掉 slot 的属性,一般都是在 slot 标签外面包一层 div,然后那些属性放到 div 的属性上
例如
这样的插槽被使用时可能会被覆盖掉 v-if 属性
<slot v-if="!isActive" name="item-icon"></slot>
而这样就不会
<div v-if="!isActive"><slot name="item-icon"></slot></div>
效果
点击每一个 item 对应一个路由
components 文件夹和 view 的区别
components 放的是公共组件
而 view 放的是单独组件
新建四个组件
在双 src 文件夹下新建 view 文件夹,然后分别新建 home、category、shopcar、profile 文件夹放对应组件
比如
home 文件夹下的 Home.vue
<div>首页</div>
新增路由
在 src 文件夹下新建一个 router 文件夹并在该文件夹下新建一个 index.js
index.js
// 1.导入
import Vue from "vue";
import VueRouter from "vue-router";
// 2.挂载
Vue.use(VueRouter);
// 懒加载
const Home = () => import("../view/home/Home.vue");
const Category = () => import("../view/Category/Category.vue");
const ShopCar = () => import("../view/shopcar/ShopCar.vue");
const Profile = () => import("../view/profile/Profile.vue");
// 3.创建路由配置对象
const routes = [
{
path: "",
redirect: "/home",
},
{
path: "/home",
components: Home,
},
{
path: "/category",
components: Category,
},
{
path: "/shopcar",
components: ShopCar,
},
{
path: "/profile",
components: Profile,
},
];
// 4.实例化路由对象
const router = new VueRouter({
routes,
});
// 5.默认导出
export default router;
main.js 导入路由
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
// 导入组件库
import ElementUI from "element-ui";
// 导入组件相关样式
import "element-ui/lib/theme-chalk/index.css";
// 配置Vue插件
Vue.use(ElementUI);
Vue.config.productionTip = false;
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
父组件给子组件传路径
App.vue
<div id="app">
<router-view></router-view>
<tab-bar>
<tab-bar-item path="/home">
<img slot="item-icon" src="./assets/img/tabbar/home.svg" alt="" />
<img
slot="item-icon_active"
src="./assets/img/tabbar/home_active.svg"
alt=""
/>
<div slot="item-text">首页</div>
</tab-bar-item>
<tab-bar-item path="/category">
<img slot="item-icon" src="./assets/img/tabbar/category.svg" alt="" />
<img
slot="item-icon_active"
src="./assets/img/tabbar/category_active.svg"
alt=""
/>
<div slot="item-text">分类</div>
</tab-bar-item>
<tab-bar-item path="/shopcar">
<img slot="item-icon" src="./assets/img/tabbar/shopcart.svg" alt="" />
<img
slot="item-icon_active"
src="./assets/img/tabbar/shopcart_active.svg"
alt=""
/>
<div slot="item-text">购物车</div>
</tab-bar-item>
<tab-bar-item path="/profile">
<img slot="item-icon" src="./assets/img/tabbar/profile.svg" alt="" />
<img
slot="item-icon_active"
src="./assets/img/tabbar/profile_active.svg"
alt=""
/>
<div slot="item-text">我的</div>
</tab-bar-item>
</tab-bar>
</div>
TabBarItem.vue
props: {
path: String
},
methods: {
itemClick () {
this.$router.push(this.path)
}
}
点击 item 才激活
将 TabBarItem 的 isActive 改成计算属性
computed: {
isActive () {
return this.$route.path.indexOf(this.path) !== -1
}
}
如何让使用者修改激活样式?
比如想修改激活文字样式 activeColor="blue"
<tab-bar-item path="/home" activeColor="blue">
<img slot="item-icon" src="./assets/img/tabbar/home.svg" alt="">
<img slot="item-icon_active" src="./assets/img/tabbar/home_active.svg" alt="">
<div slot="item-text">首页</div>
</tab-bar-item>
先在 TabBarItem 里添加一个自定义属性 activeColor
修改控制文字的那个插槽绑定的样式
TabBarItem.vue
<div :style="activeStyle">
<slot name="item-text"></slot>
</div>
props: {
activeColor: {
type: String,
default: 'red'
}
},
computed: {
activeStyle () {
return this.isActive ? { color: this.activeColor } : {}
}
}
组件化开发
组件中的 data
为什么组件中的 data 是函数?
- 当 data 是函数时,每次使用组件都会返回一个新的 data 对象,使用独立的地址空间
- 如果不是函数,那复用组件时 将共用数据源,不符合组件化思想
props
用于父组件向子组件传数据
本质就是给子组件添加自定义属性
对象形式
父组件
<son :msg="msg"></son>
data() {
return {
msg: ['a', 'b', 'c']
}
}
子组件 son.vue
<span v-for="item in msg" :key="item">{{ item }}</span>
props: {
msg: {
type: Array,
default() {
return []
}
}
}
这样,子组件就可以使用父组件传过来的数据啦
emits(vue3)
vue3 新增,用来定义一个组件可以向其父组件发射的事件。用法与 props 类似
数组 | 对象
vue2 的做法
- 在子组件中,通过**$emit()**来发射事件
- 在父组件中,通过v-on来监听子组件事件
子组件son.vue
<buttom @click="increment">+1</buttom>
emits: ['add'],
methods: {
increment() {
this.$emit('add')
}
}
父组件
<h2>{{ counter }}</h2>
<son @add="addOne"></son>
data() {
return {
counter: 0
}
},
methods: {
addOne() {
this.counter ++
}
}
vue3 的做法
数组形式
子组件son.vue
<buttom @click="increment">+1</buttom>
emits: ['add'],
methods: {
increment() {
this.$emit('add')
}
}
父组件
<h2>{{ counter }}</h2>
<son @add="addOne"></son>
data() {
return {
counter: 0
}
},
methods: {
addOne() {
this.counter ++
}
}
对象形式
对象写法的目的是进行参数验证
子组件son.vue
<buttom @click="increment">+1</buttom>
emits: {
add: null
},
methods: {
increment() {
this.$emit('add')
}
}
父组件
<h2>{{ counter }}</h2>
<son @add="addOne"></son>
data() {
return {
counter: 0
}
},
methods: {
addOne() {
this.counter ++
}
}
null 表示无参
当有参数时
emits: {
add: (num1, num2) => {
return true;
};
}
对参数有限制时(比如大于第一个参数得大于 10)
emits: {
add: (num1, num2) => {
if (num1 > 10) {
return true;
}
return false;
};
}
虽然还是能传过去,但是会有警告;这样会清楚地知道传递的参数是有问题的
提示
官网强烈建议使用 emits
记录每个组件所触发的所有事件。
这尤为重要,因为移除了 .native
修饰符。任何未在 emits
中声明的事件监听器都会被算入组件的 $attrs
中,并将默认绑定到组件的根节点上。
provide 和 inject
用于非父子组件之间共享数据
如果通过 props 逐级往下传,将会非常麻烦。
无论层级结构有多深,父组件都可以作为其所有子组件的依赖提供者
父组件有一个provide选项来提供数据
子组件有一个inject选项来使用这些数据
这个和 props 有什么区别呢?
- 父组件不需要知道哪些子组件使用了 provide 的 property
- 子组件不需要知道 inject 的 property来自哪里
基本用法
父组件
provide: {
name: 'zsf',
age: 18
}
子孙组件
<h3>{{ name }} {{ age }}</h3>
inject: ["name", "age"];
使用 data 里面的数据
要想 provide 使用 data 里面的数据,并且通过 this 拿到
data() {
return {
names: ['zsf','aaa']
}
},
provide() {
return {
length: this.name.length,
}
}
如果不写成函数,那this指向的就不是组件实例
处理响应式
如果改变 names 的长度,你会发现 provide 里面的 length 没有更新。
那要想它能做到更新,需要用到 vue 的 computed()
import { computed } from 'vue'
provide() {
return {
length: computed(() => this.name.length),
}
}
事件总线
vue2 的事件总线
初始化
第一种方式
将一个空的 vue 对象挂载到 Vue 原型上,这样每个组件对象都可以使用~
Vue.prototype.$EventBus = new Vue();
第二种方式
创建一个模块 Bus.js,导出一个空的 vue 对象,需要就导入
// Bus.js
import Vue from "vue";
export const EventBus = new Vue();
实质上,它是一个不具备 DOM 的组件,它具有的仅仅只是组件的实例方法而已,因此它非常的轻便。
发送和接收事件
EventBus.$emit('emit事件名',数据)
发送EventBus.$on("emit事件名", callback(payload1,…))
接收
举例导入 Bus.js 模块的方式通过事件总线传递信息
A.vue
<p>{{msgB}}</p>
<button @click="sendMsgA()">-</button>
import { EventBus } from "../Bus.js"
data(){
return {
msg: ''
}
},
mounted() {
EventBus.$on("bMsg", (msg) => {
// a组件接受 b发送来的消息
this.msg = msg;
});
},
methods: {
sendMsgA() {
EventBus.$emit("aMsg", '来自A页面的消息'); // a 发送数据
}
}
B.vue
<p>{{msgA}}</p>
<button @click="sendMsgB()">-</button>
import { EventBus } from "../event-bus.js"
data(){
return {
msg: ''
}
},
mounted() {
EventBus.$on("aMsg", (msg) => {
// b组件接受 a发送来的消息
this.msg = msg;
});
},
methods: {
sendMsgB() {
EventBus.$emit("bMsg", '来自b页面的消息'); // b发送数据
}
}
如果只想接收一次,可以使用EventBus.$once('事件名', callback(payload1,…)
优缺点
优点
- 解决了多层组件之间繁琐的事件传播。
- 使用原理十分简单,代码量少。
缺点
- vue 是单页面应用,如果在某一个页面刷新了之后,与之相关的 EventBus 会被移除,这样可能出现一下 意外 bug
- 如果有反复操作的页面,EventBus 在监听的时候就会触发很多次,也是一个非常大的隐患。通常会用到,在 vue 页面销毁时,同时移除 EventBus事件监听。
- 由于是都使用一个 Vue 实例,所以容易出现重复触发的情景,两个页面都定义了同一个事件名,并且没有用$off 销毁(常出现在路由切换时)。
vue3 的事件总线
vue3 从实例中移除了$on、$off、$once方法,如果想使用全局事件总线,要通过第三方库
官方推荐mitt或tiny-emitter
使用 mitt(vue3)
安装
npm instal mitt
封装一个工具 eventBus.js
import mitt from "mitt";
const emitter = mitt();
export default emitter;
发送组件 sent.vue
<buttom @click="btnClick"></buttom>
import emitter from './eventBus.js'
methods: {
btnClick() {
emitter.emit('zsf',参数)
}
}
接收组件 accept.vue
import emitter from './eventBus.js'
created() {
emitter.on('zsf', (参数) => {
拿到参数
})
}
写法与 Vue2 类似,不过是使用了第三方库
多个事件的发射与监听
发送组件 sent.vue
import emitter from './eventBus.js'
methods: {
btnClick() {
emitter.emit('zsf',参数)
emitter.emit('aaa',参数)
}
}
接收组件 accept.vue
import emitter from './eventBus.js'
created() {
emitter.on('zsf', (参数) => {
拿到参数
})
emitter.on('aaa', (参数) => {
拿到参数
})
}
$refs
这种方式只需要在**需要访问的子组件或元素上加个 ref="xxx"**的属性
可以通过this.$refs访问到子组件或元素的信息
ref 在元素上
父组件
<h2 ref="h"></h2>
这样,父组件就可以通过this.$refs.h获取到h2 的元素对象
ref 在组件上
父组件
<son ref="item"></son>
这样,父组件就可以通过this.$refs.item获取到组件 son 的实例对象啦,子组件的信息都可以拿到(data、methods 等等)
插槽
动态插槽名
后面补充~
作用域插槽
先来看看什么是渲染作用域
- 父级模板里的所有内容都是在父级作用域中编译的
- 子级模板里的所有内容都是在子级作用域中编译的
比如有个父组件包了一个子组件,子组件有个title数据,想直接在父组件里面显示 title,这是不可以的。
这就是渲染作用域
但是,有时候我们希望插槽可以访问到子组件中的内容
常见应用:
当一个组件用来渲染一个数组元素时,又想使用插槽,并且希望插槽中显示每项内容
父组件
<son :names="names"></son>
data() {
return {
names: ['zsf', 'abc', 'sss']
}
}
展示组件 son.vue
<template v-for="item in names" :key="item">
<span>{{ item }}</span>
</template>
props: {
names: {
type: Array,
default: () => []
}
}
一般情况是这样的。但是,要是父元素不想使用 span 展示,想用其它元素展示(换句话说,父元素使用 son 组件时,可以决定使用什么元素展示)
展示组件 son.vue
<template v-for="item in names" :key="item">
<slot>{{ item }}</slot>
</template>
父组件这样写对吗?
<son :names="names">
<button>{{ item }}</button>
</son>
不对。由于存在渲染作用域,button 访问不到 slot 内部的 item
这时你可能会问:为什么不直接在父组件遍历并展示?
上面有说到:我们希望通过复用其它组件展示,又想使用插槽。。。
这就用到作用域插槽了
用法
展示组件 son.vue,在定义插槽时声明
<template v-for="(item, index) in names" :key="item">
<slot :item="item" :index="index">{{ item }}</slot>
</template>
父组件这样写
<son :names="names">
<template v-slot="slotPros">
<button>{{ slotPros.item }}</button>
</template>
</son>
这样,slotPros可以拿到 slot 定义的那些属性(item、index)
动态组件
基本用法
使用component这个内置组件的is属性
is 的值可以是局部注册过的组件,或者全局注册过的。
标签栏切换案例
<div id="dynamic-component-demo">
<button
v-for="tab in tabs"
:key="tab"
:class="{ active: currentTab === tab }"
@click="currentTab = tab"
>
{{ tab }}
</button>
<component :is="currentTab"></component>
</div>
components: {
Home,
Posts,
Archive
},
data() {
return {
currentTab: 'Home',
tabs: ['Home', 'Posts', 'Archive']
}
}
给动态子组件传值
直接在 component 组件加上属性即可,就是把要传的值当 component 的属性
<component :is="currentTab" name="zsf" :age="18"></component>
这样,切换到的组件都会拿到 name 和 age,通过props拿到;
当然,动态子组件也可以通过emits给父组件传事件
状态缓存
你有没有想过这样一个问题:切换子组件时,要想再切回去,以前的状态会保留吗?
不会。一旦切换,上一个子组件就会被销毁,状态没了;切换回去时,是重新创建。
每一次的切换来切换去都是销毁-重建的过程,这是耗性能的一件事
能不能将组件的状态缓存起来呢?
可以。使用内置组件keep-alive包裹起来
<keep-alive>
<component :is="currentTab" name="zsf" :age="18"></component>
</keep-alive>
keep-alive 的三个属性
include
string | RegExp | Array
只有名称匹配的组件才会被缓存状态
exclude
string | RegExp | Array
匹配名称的组件不会缓存状态
max
number | string
最多可以缓存组件数量,一旦到达这数字,缓存组件最近没有被访问的实例会被销毁
提示
由于 include 和 exclude 都是根据名称匹配,所以要给对应组件加上name 选项
异步组件
某些组件在一开始用不上,打包他们时,可以进行分包,优化首屏渲染时间
defineAsyncComponent(vue3)
Vue3 提供了一个 api:defineAsyncComponent
接收两种类型参数:
- 工厂函数,该工厂函数需要返回一个 promise 对象
- 对象,可以对异步组件进行更多配置
利用 webpack 的特性在 Vue 中使用异步组件
接收工厂函数写法
import { defineAsyncComponent } from "vue";
const AsyncDetail = defineAsyncComponent(() => import("./AsyncDetail.vue"));
import()返回的就是 promise,并且会在打包时进行分包操作
接收对象的写法
import { defineAsyncComponent } from 'vue'
const AsyncDetail = defineAsyncComponent({
loader: () => import('./AsyncDetail.vue'),
...
})
更多配置可以查看官网
和 suspense 一起使用
Suspense 是一个内置的全局组件,该组件有两个插槽
- default 如果 default 可以显示,就显示 default 插槽的内容
- fallback 如果 default 无法显示,就显示 fallback 插槽的内容
<suspense>
<template #default>
<async-home></async-home>
</template>
<template #fallback>
<loading></loading>
</template>
</suspense>