关于vue的绝大部分知识点都在前端面试—vue部分 | 三叶的博客这篇文章中讲了,这里就说说基础语法
关于vue的绝大部分知识点都在前端面试—vue部分 | 三叶的博客这篇文章中讲了,这里就说说基础语法

Vue2
插值表达式
利用表达式进行插值,渲染数据到页面中
1 | <h3>{{ title }}</h3> |
注意:不能写到标签内部,表达式涉及到的数据必须存在。
指令
带有v-前缀
的特殊标签属性,Vue会根据不同的【指令】,针对标签实现不同的【功能】
v-html
设置元素的innerHTML
v-if/v-show
用来控制元素显示隐藏。
v-show=”表达式”,表达式值true显示, false隐藏
v-if=”表达式”,表达式值true显示, false隐藏
二者的区别可以参考:前端面试—vue部分 | 三叶的博客
v-else/v-else-if
配合v-if使用,v-show没有这个待遇,语法和C语言的if-else语法差不多。
v-on
注册事件。免去了手动捕获元素
,添加事件监听
的工作,只需专心书写回调逻辑
,极大地简化了事件监听的代码。
1 | v-on:事件名 = "内联语句"//js代码 |
v-bind
动态
的设置标签属性src url title …
1 | v-bind:属性名 = "表达式" |
动态样式控制
借助v-bind指令可以很方便的实现动态控制样式
:class = "数组/对象"
1
<div class="box" :class="[类名1,类名2,类名3 ]"></div>//添加到数组中表示这个类名生效
1
<div class="box" :class="{类名1:布尔值,类名2:布尔值}"></div>//布尔值为true表示添加这个类
:style ="样式对象"
1
2
3
4//属性值可以是表达式
<div class="box" :style="{CSS属性名1:CSS属性值,CSS属性名2:CSS属性值} "></div>
//原生行内写法,属性值不能是表达式,是写死的,用分号分隔样式
<div class="box" style="dispaly:none;width:100px;"></div>
v-for
基于数据循环,多次渲染整个元素,哪个标签需要多次渲染就加到哪个元素身上。
1 | <li v-for="(value, key, index) in obj"> |
关于给标签添加key
的作用,还有v-if
和v-for
优先级问题以及能否一起使用的问题,参考前端面试—vue部分 | 三叶的博客
为什么 v-for
指令中的参数比如(value,key)
,能被该标签内的其他属性使用,还能在标签体内使用?
模板作用域:Vue 的模板语法中,标签的属性和内容共享同一个作用域
参数注入:v-for
中定义的参数(如 value
和 key
)会被自动注入到当前标签及其子节点的作用域中。这意味着,只要是在这个作用域内的代码(标签属性、子元素、插值表达式等),都可以访问这些参数。
v-model
给表单元素使用,双向数据绑定,可以快速获取或设置表单元素内容。
1 | <input type="text" v-model="a"> |
本质上是一个语法糖:
1 | <input type="text" :value="a" @input="(e)=>{a = e.target.value}"> |
应用于其他表单元素:
checkbox:绑定一个布尔值变量,true表示选中
1
<input type="checkbox" :checked="a" @input="(e)=>{a = e.target.checked}">
radio
给同一组radio绑定相同的变量,点击哪个就会把哪个的value赋值给变量
1
2<input v-model="gender" type="radio" name="gender" value="1">男
<input v-model="gender" type="radio" name="gender" value="2">女
sync
1 | <!-- 父组件 --> |
sync
修饰符,通常与v-bind
指令一起使用,用来简化子组件向父组件通信的代码,就是不需要手动在组件上添加自定义事件,也不需要书写对应的回调逻辑,
而是使用提供的默认事件
,就能实现子组件向父组件传递数据,从而实现简化父子组件通信,
简单的来说,在vue2中,v-bind加上sync修饰符仿佛就实现了组件自己v-model指令
因为在vue2中直接给组件使用v-model,就意味着只能传入value属性,监听的事件只能是input,而借助sync,就能指定多个传入子组件的属性和对应的事件。所以说sync
和v-bind
就是用来解决vue2中,v-model应用在组件上功能不足的问题。
在 Vue 3 中,.sync
修饰符已经被移除,推荐的做法是使用自定义事件(就是手动给组件标签添加事件监听并传入事件回调)
或 v-model
来达到类似的效果。
vue 3 对 v-model
进行了增强,使其更加灵活,允许在一个组件上使用多个 v-model
绑定,并且可以自定义绑定的prop
和 event
名称。
默认情况
1 | /*父组件*/ |
1 | /*子组件内*/ |
$emit
是this.$emit
的简写,this指向当前组件实例
,在模板中可以省略$event
是在模板中的事件处理器
中使用的特殊变量,用于引用原生的事件对象
。而在原生dom中,可以直接使用
event
代替原生事件对象1
<a href="/about" onclick="event.preventDefault()" target="_blank">about</a>
<input @input="logValue($event)">
和<input @input="logValue">
这两种写法效果都是一样的要注意的是,给
组件
添加事件监听和给dom元素
添加事件监听,都传入一个回调函数,但不同的是,Vue 组件中的事件是自定义事件
,不是浏览器原生的 DOM 事件
,没有与之相关的event(事件)
对象,对于组件的自定义事件
的回调函数,传入的第一个参数其实是子组件通过emit传递过来的值
,后者传入的则是事件对象event
。
Vue 3 支持在一个组件上使用多个 v-model
绑定。这可以通过指定自定义的 prop 和 event 名称来实现
在vue2中的:message.sync
等价于vue3中的v-model:message
1 | /*父组件*/ |
1 | /*子组件*/ |
指令修饰符
计算属性computed
基于现有的数据,计算出来的新属性。依赖的数据变化,自动重新计算。
使用起来和普通数据一样:
{{计算属性名}}
简写:
1 | computed: { |
完整写法:
1 | computed: { |
计算属性一般只用来展示,赋值。如果尝试直接修改计算属性,并不会生效,因为计算属性的值只与其相关的数据有关,但是会把
传入的值
传递到set函数,set函数拿到这个值可以做一些操作。依赖的数据必须是响应式的(即
data
或其他计算属性),不然依赖的数据改变了计算属性也无法发觉,就不会即时更新。当计算属性依赖的任何数据发生变化时,Vue 会标记计算属性为
脏
,并在下次访问时重新计算其值。计算属性采用惰性求值策略(被访问的时候再求值),并具有缓存机制(如果依赖的数据未改变,直接使用缓存,而不需要重新计算),只有当依赖的数据发生变化,即被标记为脏
,并被访问的时候,才会重新计算其值。同时计算属性也是响应式的,当计算属性的值改变,也会通知计算属性的依赖更新。
因为计算属性和data的用法是一样的,都属于响应式数据,它们收集依赖的方式也是一样的,都是被访问的时候,通过getter方法收集。
计算属性在首次计算其值时,通过访问响应式数据,触发响应式数据的
getter
方法,成功收集计算属性为它们的依赖。
监听器watch
用来监视data
和计算属性
中数据的变化。
简写:
1 | watch:{ |
完整写法:
1 | watch: { |
deep: true
,开启对复杂类型深度监视后,可以监听一整个对象,可以监听这个对象中的全部属性,否则监听的只是对象的地址变化。
通过vue实例的$watch
方法也能添加监听
1 | vue.$watch('监听的数据',{//配置对象})//完整写法 |
watch监听的数据必须是响应式的,然而在vue2中数据默认都是响应式的。
watch可以理解为用来自定义扩充某个响应式数据的setter方法体。
watch和computed的区别与联系
watch,computed和视图,这三者都依赖响应式数据,是响应式数据的三大订阅者,而响应式数据又包括data中的数据和计算属性。
在watch里可以写异步操作,但是在computed内部不适合写异步操作,比如无法异步返回值。
1
2
3
4
5
6
7{
computed:{
sum(){
setTimeout(()=>{return this.price*this.num},1000)
}
}
}computed能做的,用watch也能实现,就是更为麻烦;但是watch能做的,computed不一定能实现,比如延迟几秒更新数据。
method和computed的区别与联系
我们讨论的是有返回值的方法
和computed
的区别于联系
- 当某个数据属性的值发生变化时,Vue 会自动检测到这个变化,并重新计算依赖于这个数据属性的任何方法,对于计算属性则是标记为’脏’
- 计算属性本质就是一个有返回值的方法
- 计算属性存在缓存机制,无论被使用多少次,只要依赖的数据未改变,计算属性函数只会被调用一次;而方法被使用多少次就会被调用多少次
- method在模板内使用需要显式的调用,而computed直接当成data的使用。
MVVM
model-view-viewmodel
model
层负责管理应用程序的业务逻辑和数据,不包含任何的ui逻辑,类似小程序中的逻辑层
view
View 负责显示数据给用户;是应用程序直接与用户直接交互的部分,类似小程序中的视图层
viewmodel
ViewModel 作为 Model 和 View 之间的桥梁,负责将数据转换成view可以使用的格式。在vue中,这个桥梁(vm)就是vue实例。
Vue3
setup()
vue3中的一个新的配置项,值为一个函数,组合式api都写在这里面。
执行时机在
beforeCreate()
之前;而beforeCreate的执行时机又再data函数被调用前。setup中不能使用this,this的值是undefined;
setup不能是
async
函数,因为async函数的返回值会被Promise.resolve
包装
在组合式api中的写法
在组合式api
中,setup()
以下面的方式书写,格式类似生命周期函数。
setup中准备的数据
和函数
需要在setup最后return
后,才能在模板中应用 。
配置了setup,还能配置data
和methods
,同时在methods里面也能读取到setup中的配置,当data中和setup中存在数据冲突,setup中的数据优先级更高,但是还是建议vue2的配置和vue3的配置不要混用。
1 | <script> |
其实还能返回一个渲染函数,不过用的很少。
1 | <script> |
然后这个组件
就会使用这个函数来渲染,而忽略模板结构。
语法糖写法
1 | //独占一个script标签,不需要return |
setup的参数
setup的参数在混合选项式api的时候,也就当不使用setup语法糖
开发的时候是有意义的,使用语法糖开发的时候参数都没了。
setup(props,context)
props
是第一个参数,值为对象,包含组件外部传入组件的,且在组件内部接收的值,也就是在props属性
中接受的值。
这个参数的作用就在于,让通过选项式api中的props属性接收的值,能够在setup函数内部使用。
这些属性是响应式的,因此当父组件更新 props
时,子组件中的 props
也会自动更新。
1 | <template) |
context
第二个参数,上下文对象,包含多个属性
attrs
:值为对象,包含组件外部传入组件的,但在组件内部未被接收的值
,因为这些值会被当成组件的属性,所以存储context.attrs
中,类似vue2中的this.$attrs
(用来获取组件实例的属性)emit
:值为函数,相当于this.$emit
,用来触发自定义事件
要注意的是,在vue3中,给组件添加的事件监听,默认都是
原生事件
,如果需要指定为自定义事件
,需要在子组件中通过emits
属性声明,或者使用组合式api中的defineEmits中指定(返回emit函数)
1
context.emit('hello',666)
slots
:收到的插槽内容,类似vue2中的this.$slots
,可以访问到父组件
通过插槽
传递的所有内容。this.$slots
是一个对象,其键名对应于插槽的名字
(对于默认插槽,键名为default
),值则是包含一组VNode
的数组。
简单的来说,context参数弥补了在setup函数内部因为不能使用this,而缺失的部分功能,比如this.$emit
可以被替换为context.emit
,this.$slot
可以被替换为context.slots
。
reactive和ref
vue3中数据默认不是响应式的,需要手动添加响应式。
reactive
- 接受
对象类型
数据的参数,并返回一个响应式
的对象,就是Proxy类型的对象。 - 传入一个源对象,经过proxy操作返回一个代理对象,修改代理对象会映射到源对象。
1 | <script setup> |
源码分析:
修改代理对象会映射到源对象这一点,在传入的第一个参数:配置对象上就能看出,操作对象一直是target。
1 | function reactive(target) { |
ref
接受
简单类型
或者复杂类型数据
,返回一个响应式对象
,就是RefImpl
类型的对象,如果传入的值是对象,那ref.value
的类型就是proxy
。ref在内部其实会使用
reactive
(如果传入的是一个对象),总的来说基于proxy
实现。RefImpl
实例结构分析:下面三个属性是并列关系
value
:不可枚举属性,访问实际是在调用get函数
,返回_value
中的数据。_value
:访问value返回的实际数据_rawValue
:源数据,传入ref的原始数据
,raw
的中文意思就是”未经加工的,原始的”;当原始数据是简单类型,_rawValue
等于_value
,当传入的是对象,_rawValue
的值始终是对象,_value
的值则是proxy
对象,修改_value
会映射到_rawValue
上。


至于value
属性对应的getter,setter
,存在于[[prototype]]
对象中。
大致源码如下:
1 | function ref(val){ |
1 | class RefImpl { |
我们可以观察到,构造函数中并没有初始化value
,但是refImpl
实例中出现了,我自己测试了一下,发现并没有借助defineProperty
1 | //自定义一个refImpl类 |
最后打印p的结构如图:

观察到,value属性出现了,这就说明,是通过在原型上挂载get value()
和 set value(val)
定义了value属性。
注意:
当我们直接给
.value
赋予一个新的对象,vue会帮我们重新
创建一个响应式对象1
2
3
4this._rawValue = toRaw(newVal);//如果传入的本身是响应式对象,则获取它的原始值
//重新创建一个响应式对象,如果传入的本身是响应式对象,那toReactive方法如何处理呢?Vue 会直接使用它,不会重新代理
//无论赋予value的是ref,reactive包装的对象,还是普通对象,最终this._value的值都是proxy对象
this._value = useDirectValue ? newVal : toReactive(newVal);脚本中访问ref数据,需要通过
.value
,而在template
中,.value
不需要加(帮我们扒了一层),就好比在vue2的模板内,可以省略this
,比如this.$store
在模板内直接写为$store
,其实data
中的数据能直接在模板中使用,也是省略了this。
toRef()和toRefs()
响应式丢失
下面举个例子来说明:
1 | <template> |
我们点击num++
,发现视图中的obj
和txt
都被更新了,输出txt
发现是个Proxy
类型的对象,说明响应式
并没有丢失。
我们点击age++
,发现视图中的age
并没有随之改变,输出age
,发现是简单数据类型,说明响应式丢失了。
因此我们可以得出,vue3是递归
给对象添加响应式的(其实是需要的时候再递归添加),解构对象并不是所有属性的响应式都会丢失。
虽然我们对ref
和reactive
已经比较了解了,为了保险起见,我们将ref
替换为reactive
再测试一下,果然,现象完全一样。
其实在vue2
中,也存在着响应式丢失的现象,我们知道vue2是响应式实现是基于Object.defineProperty
方法的,当vue2给一个对象添加响应式,如果这个对象中还包含对象,则也会递归添加响应式。下面是一个例子来说明这一点:
1 | export default { |
上述代码会输出'tom'
和一个响应式对象child:

toRef()
toRef()
函数可以将一个响应式对象(ref或者reactive包装的对象)的某个属性转换成一个ObjectRefImpl
(注意是返回一个新的ObjectRefImpl对象,并不会修改任何对象),从而解决对象解构响应式丢失的问题。
1 | import { reactive, toRef, ref } from 'vue'; |
a.count
和countRef
的共用一份源数据。输出countRef
:

其中_object
参数toRef
传入的第一个参数,_key
就是toRef
传入的第二个参数,当我们访问value的时候,返回的就是_object[_key]
,当我们修改value的时候,其实就是在修改_object[_key]
,而_object
本身就是一个代理对象。
1 | get value() { |
toRefs()
toRefs()
函数可以将整个响应式对象的所有属性都转换为refs
的集合。这对于解构响应式对象特别有用,因为直接解构会导致某些属性丢失响应性。直接解构就等同于取值然后赋值给新的变量,我们取值取的数据来源
就是源对象
,而源对象中的部分属性值不具有响应式
的。
1 | import { reactive, toRefs } from 'vue'; |
可以看出stateAsRefs
对象就是一个普通的对象,不过它的每个属性的值都变成了ObjectRefImpl
类型的数据。

computed
1 | import { computed } from 'vue' |
注意:
- vue3中的computed属性默认是响应式的
- 避免直接修改计算属性的值,计算属性应该是只读的,特殊情况可以配置get,set
- 计算属性中不应该有
副作用
,不建议在计算属性中写dom操作和异步请求
与vue2计算属性区别:
- vue2使用computed不需要导入,是一个配置属性;vue3中的computed是一个函数,需要按需导入。
- vue2中
批量添加
计算属性更方便,在vue3中每次获得一个计算属性都要传入一个回调函数,调用一次computed
函数
watch
vue3中使用watch监听ref包装的数据,是存在一定问题的。
watch能正常监听通过ref(基本类型数据)
得到的对象的改变,比如ref(0)
;但是监听不到ref(引用类型数据)
得到的对象的改变。
比如ref({ age: 0 })
,虽然这个问题可以通过开启深度监听解决(在第三个参数传入{ deep: true }
),或者监听ref.value(返回一个reactive包装的对象),但是存在新旧数据相同的问题。
watch监听reactive类型的数据默认是深度监听
,没有太大的问题,但也存在新旧数据相同的问题。
如果不关心新旧数据的问题,监听reactive包装的数据可以认为是没有问题的,但是监听ref包装的引用数据,就会出现监听不到的问题。
1 | import { watch } from 'vue' |
与vue2中watch的区别
- vue2中watch是属性,vue3中是需要导入的函数
- 在vue2的watch中的函数名就是监听的对象,执行的操作是函数体
- vue2中添加多个监听对象只需要都写在watch:{}中就行,vue3中可以放在数组中整体监听
watchEffect
watch
的套路是:既要指明监视的属性,也要指明事件的回调。watchEffect
的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。watchEffect
有点像computed
- 但
computed
注重的是计算出来的值(回调函数的返回值),所以必须要写返回值。 - 而
watchEffect
更注重的是过程(回调函数的函数体),所以不用写返回值。
- 但
1 | // watchEffect 所指定的回调中用到的数据只要发生变化,则直接重新执行回调。 |
生命周期函数
在vue3中,既支持选项式
生命周期函数,也支持组合式
生命周期函数,但是要注意的是,在vue3中即便能使用选项式风格的生命周期函数,也与vue2中的不完全相同。
选项式
beforeCreate
:实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。created
:实例创建完成后被调用。此时已完成数据观测 (data observer),属性和方法的运算,watch/event 事件回调。但是尚未挂载,$el
属性目前不可见。beforeMount
:在挂载开始之前被调用,相关的render
函数首次被调用。mounted
:实例挂载到 DOM 后调用,这时el
被新创建的vm.$el
替换,并挂载到实例上。注意,不能保证它在整个组件树完全渲染完成时才调用。beforeUpdate
:在数据更新导致虚拟 DOM 重新渲染和打补丁之前调用。你可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。updated
:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。beforeUnmount
(在 Vue 2 中称为beforeDestroy
):在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。unmounted
(在 Vue 2 中称为destroyed
):卸载组件后调用。调用此钩子时,组件实例的所有指令都被解绑,所有事件监听器都被移除,所有子组件实例也都会被销毁。
简单的来说,vue3的选项式生命周期函数与vue2的生命周期函数的区别仅仅在于最后两个。
组合式
在 Vue 3 的组合式 API 中,你可以使用 onXXX
形式的函数,来注册生命周期钩子。这些函数可以直接在 setup()
函数或 <script setup>
中使用。
onBeforeMount
onMounted
onBeforeUpdate
onUpdated
onBeforeUnmount
onUnmounted
Vue 3 中,setup
函数涵盖了 beforeCreate
和 created
钩子的功能,因此不再需要这两个钩子。
1 | import { onMounted, onUpdated, onUnmounted } from 'vue'; |
组件通信
props与emit
父传子
父组件给子组件,添加属性的方式传值。
1 | <template> |
在子组件,通过props接收,借助于编译器宏
1 | //使用编译器宏不需要导入 |
与vue2的区别
语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//在vue2中
export default {
props: {
title: String,
count: {
type: Number,
default: 0,
validator: (v) => v >= 0
}
}
}
//在vue3中
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
title: String,
count: {
type: Number,
default: 0,
validator: (v) => v >= 0
}
});
</script>响应式处理
在vue2中,Props 数据通过
Object.defineProperty
实现响应式;在vue3中基于 Proxy 的响应式系统,自动处理 Props 的响应式绑定,支持动态属性监听和更高效的数组更新。
子传父
父组件中给子组件标签通过@
绑定事件
1 | <template> |
子组件内部通过emit
方法触发事件
1 | <script setup> |
在vue3中,父子组件通信,其实用v-model
也能实现,详细介绍参考前文v-model
的部分
1 | <template> |
1 | <script setup> |
与vue2的区别
- 无法直接通过this调用
$emit
函数,需要通过编译器宏获得emit函数。 - 在vue2中可以通过
<component @click.native></component>
,表示添加的点击事件是原生事件,而不是自定义事件;在vue3中这种写法被移除了,给组件添加的事件默认是原生的。
provide与reject
顶层通过provide
提供数据,底层通过inject
接收数据,数据传递是单向的
provide
1 | import { provide, ref } from 'vue'; |
inject
1 | import { inject } from 'vue'; |
响应式
当使用 provide
提供的数据是响应式的(例如通过 ref
或 reactive
创建),那么在数据更新时,所有通过 inject
接收到该数据的组件都会自动更新
。这是因为 provide
/inject
维护的是对原始数据的引用,我们认为这些组件依赖这些注入的数据。
与vue2的区别与联系
vue2中可以通过
inject:[]
来批量接收数据,vue3中只能一个一个接收。vue2中可以在
provide(){ return{//传递的数据,格式,键:值}}
中批量传递数据,vue3中只能一个一个的传递数据和vue3中相同的是,在vue2中通过provide传递的如果是响应式数据,当父组件修改这个数据的时候,注入了此响应式数据的子组件也会更新。
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
41
42
43
44
45
46
47
48
49
50
51
52
53<template>
<div>
num:{{num}}age:{{age}}
mom:{{ parents.mom }}
<button @click="add('num')">num++</button>//点击,只有父组件视图改变
<button @click="add('age')">age++</button>//点击,父子组件视图都改变
<button @click="add('mom')">mom++</button>//点击,父子组件视图都改变
//props
//即便父组件通过props传递给子组件的是基本数据类型(this.age),如果子组件展示了这个数据
//在父组件中修改这个数据,子组件也会触发视图更新
//因为传递这个数据的时候,其实就确定了哪个组件依赖它,所以能成功收集依赖
//provide
//而通过provide传递的数据,没有明确的依赖关系,因为父组件provide了数据,也可能没有任何子组件inject
//但是如果provide的是一个响应式对象,传递的是引用,父子组件共享的是同一份数据
//在父组件中修改数据,子组件中的此数据也会改变(因为是同一份数据)
//然后因为是响应式数据,子组件中在访问这个响应式数据的时候,触发getter,就会被这个响应式数据收集为依赖。
//当父组件修改此数据,触发setter,就会通知依赖更新,这个依赖就包括子组件
<HelloWorld :age="age"></HelloWorld>
</div>
</template>
<script>
import HelloWorld from '@/components/HelloWorld.vue';
export default {
data(){
return {
num:1,
age:1,
parents:{mom:43}
}
},
provide(){
return {
num:this.num,//传递的只是一个值
parents:this.parents//传递的是一个对象,一个响应式对象
}
},
components:{
HelloWorld
},
methods:{
add(str){
if(str=='num'){
this.num++
}else if(str=='age'){
this.age++
}else{
this.parents.mom++
}
}
}
}
</script>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<template>
<div class="hello">
num:{{num}}age:{{age}}
parent:{{ parents.mom }}
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
age: Number
},
inject:['num',"parents"]
}
</script>
模板引用
步骤
- 调用ref函数生成一个ref对象:
const inp = ref(null)
- 通过ref标识,进行绑定
<input ref="inp" type="text">
- 通过
ref对象.value
即可访问到绑定的元素
与vue2中ref/$ref的区别与联系
- vue2中通过给
子元素
添加ref属性
并任意赋值命名,然后通过this.$ref.属性名
就能获取到绑定的dom或者组件实例 - 总的来说,都是给dom元素或者组件添加ref属性,然后赋一个值;在vue2中这个值是一个独一无二的名字,在vue3中这个值就是一个
RefImpl
类型的数据;然后都要拿到dom元素或者组件实例,在vue2中是通过this.$ref.属性名
拿到,在vue3中是通过RefImpl.value
拿到 - 作用范围都是当前组件内
defineExpose()
默认情况下在<script setup>
语法糖下,组件内部的属性和方法是不开放给父组件访问的,即便能够拿到组件实例也访问不了。
可以通过defineExpose
编译宏指定哪些属性和方法允许访问
1 | <script setup> |
而在vue2中,只要获取到了组件对象,就能访问里面的属性(data)和方法(methods)。
事件修饰符
stop:阻止事件冒泡,等在传入的回调函数中添加
event.stopPropagation()
1
2
3
4
5
6<button @click.stop="handleClick">点击不会冒泡</button>
//等效于
const handleClickWithStop = (event) => {
event.stopPropagation(); // 手动阻止冒泡
// 其他业务逻辑
};prevent:阻止默认行为,等同于在传入的回调函数中添加
event.preventDefault()
1
<form @submit.prevent="handleSubmit">提交表单不会刷新页面</form>
capture:使用事件捕获模式(默认是冒泡模式)
1
<div @click.capture="parentClick">父级先触发</div>
self:仅当事件从元素本身(而非子元素)触发时执行
1
<div @click.self="onlySelfClick">点击子元素不触发</div>
once:事件只触发一次,之后自动移除对该事件的监听,避免因长期持有未使用的监听函数导致内存泄漏、
1
<button @click.once="oneTimeAction">仅首次点击有效</button>
其实在原生dom事件中,实现这个效果也是非常简单的,只需要在第三个参数传入
{ once: true }
,手动通过removeEventListener还是比较消耗精力的,不过灵活度更大。1
element.addEventListener('click', handler, { once: true });
passive:提升滚动性能,不与
prevent
同时使用1
<div @scroll.passive="onScroll">滚动更流畅</div>
当监听
touchstart
、touchmove
或wheel
(滚动)等高频事件时,浏览器的默认行为是:等待事件处理函数执行完毕再决定是否执行默认行为(如滚动页面),如果事件处理函数中存在耗时操作(如复杂计算),会导致 滚动卡顿,因为浏览器必须等待函数执行完毕才能滚动页面(默认行为)。
passive
修饰符的作用,是通过将事件监听器标记为 被动模式(Passive),本质是向浏览器承诺:
“此事件处理函数不会调用event.preventDefault()
”,从而允许浏览器 立即触发默认行为,无需等待函数执行。Vue 3 的
.passive
修饰符对应原生addEventListener
的{ passive: true }
配置:1
2// Vue 编译后的等效代码
element.addEventListener('scroll', handler, { passive: true });.passive
向浏览器承诺 不会阻止默认行为,而.prevent
的作用是 主动阻止默认行为,二者语义冲突,所以不能同时使用。
Vue-Router
在vue2,vue3项目,使用的路由插件都是vue-router
,就是语法有所不同。
修改地址栏路径时,切换显示匹配的组件
单页面应用
所有功能在一个html页面上实现,基于前端路由实现
优点:按需更新性能高,开发效率高,用户体验好
缺点:学习成本,首屏加载慢(如果不使用代码分割,加载首屏还会将其他页面的资源一同加载并处理,然后才开始首屏渲染),还不利于SEO。
路由导航
声明式导航router-link
使用vue-router提供的全局组件router-link
,替代a标签实现跳转,必须配置to属性指定路径(不需要加#)。本质还是a标签。
1 | <router-link to="/路径值"</router-link> |
能高亮,默认就会提供高亮类名,可以直接设置高亮样式,不需要手动添加类名,高亮类名包括:
router-link-active(模糊匹配,常用)
to="/my"
可以匹配/my/b
,意思是当前页面的前端路由是/my/b
,这个高亮类名就会生效。router-link-exact-active(精确匹配):就必须完全一样,这个高亮类名才会生效。
编程式导航
1 | this.$router.push() |
使用path跳转
push字符串
1
2
3this.$router.push('路由路径')
this.$router.push('/路径?参数名1=参数值1&参数2=参数值2')//传入查询参数
this.$router.push('/路径/参数值')//动态路由传参push对象
1
2
3
4
5
6this.$router.push({path: '路由路径'})
this.$router.push({
path: '/路径',
query: {参数名1:'参数值1',参数名2:'参数值2'}
})//传入查询参数
this.$router.push({path:'/路径', params:{参数名:参数值}})
使用name跳转
适合路径过长的路由,给path路径取名,用name替代path,好处是不用写过长的路径,缺点是只能通过push对象的方式跳转,传参,因为没有path无法拼接成合法的url。
1
2
3
4
5
6
7
8
9
10
11
12
13const routes = [
{
path: '/',
name: 'Home', // 给首页路由命名为 'Home'
component: Home,
},
{
path: '/about',
name: 'About', // 给关于页面路由命名为 'About'
component: About,
},
// 其他路由...
];1
2
3
4
5
6
7
8
9
10
11this.$router.push({
name:"路由名字'
})
this.$router.push({
name:"路由名字',
query: {参数名1:'参数值1',参数名2:'参数值2'}
})
this.$router.push({
name:"路由名字',
params: {参数名1:'参数值1',参数名2:'参数值2'}
})
参数接受
this.$route.query:接收查询参数
this.$route.params:接收动态参数
动态路由传参的前提是组件配置了动态参数。
1
2
3
4
5
6
7const routes = [
{
path: '/about/:a',
component: About,
}
// 其他路由...
];然后跳转传参:
to="/about/3"
,3就会被赋值给a,然后在About组件中,通过this.$route.params.a
访问。如果跳转不穿参:
to="/about"
,就会报错,如果希望可传参,可不传,则在动态参数后加上?
。1
2
3
4
5
6
7const routes = [
{
path: '/about/:a?',
component: About,
}
// 其他路由...
];动态路由的其他意义:让不同的路由对应相同的组件。
嵌套路由
children
属性用于定义嵌套路由。每个子路由的 path
应该相对于其父路由的路径来理解。
1 | { |
重定向redirect
1 | const routes = [ |
路由出口router-view
router-view
是vue-router提供的一个全局的组件,是一个可以被替换掉的动态组件
。
导航守卫

next()
- 无参数:直接调用
next()
表示允许导航继续进行,效果和next(true)
是完全等价的,都表示允许导航继续。 - 传递路径或命名路由:
next('/somePath')
或next({ name: 'SomeRoute' })
用于重定向到另一个位置。 - 传递 false:
next(false)
阻止导航继续进行。 - 传递错误对象:
next(error)
触发路由错误处理逻辑。
局部守卫
组件内部
beforeRouteEnter
1
2
3
4
5
6
7
8
9export default {
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被confirm 前调用
// 不能获取组件实例 `this`,因为当守卫执行前,组件实例还没被创建
next(vm => {
// 通过 `vm` 访问组件实例
})
}
}beforeRouteLeave
这个守卫用来阻止用户离开当前路由。比如,你可以用它来提示用户是否有
未保存的更改
。1
2
3
4
5
6
7
8
9
10export default {
beforeRouteLeave(to, from, next) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (answer) {
next()//确认离开
} else {
next(false)//取消离开
}
}
}beforeRouteUpdate
这个守卫在当前路由改变,但是该组件被复用时调用。举例来说,对于一个带有动态路由参数的路径
/foo/:id
,当你从/foo/1
导航到/foo/2
时,由于会使用同一个组件实例,所以beforeRouteUpdate
守卫会在这种情况下被调用。但是如果这个组件实例被缓存了,从别的组件切换到这个组件也不会触发这个钩子。只能借助
activated
或者beforeRouteEnter
1
2
3
4
5
6
7
8
9
10
11
12export default {
beforeRouteUpdate(to, from, next) {
// 路由改变时重新获取数据
this.fetchData()
next()
},
methods: {
fetchData() {
//获取数据的逻辑
}
}
}可以观察到组件内的路由守卫都以
beforeRoute
为前缀,而全局前置守卫和路由独享守卫都只以before
为前缀
路由独享守卫beforeEnter
这个路由守卫是在配置路由的时候书写的,也是路由对象的一个属性,和path,component等是同一级别。
1
2
3
4
5
6
7
8{
path: '/home',
component: home,
beforeEnter:(to,from,next)=>{
console.log(to,from,next)
next()
}
},//这里的to显然是home组件的路由对象,即this.$route
全局守卫
beforeEach
beforeEach
即全局前置守卫,在每次导航时都会触发,无论是从一个路由跳转到另一个路由,还是首次进入应用。所有的路由在真正被访问到之前(解析渲染对应组件页面前),都会先经过全局前置守卫,只有全局前置守卫放行了,才会到达对应的页面,或者说才会开始渲染对应的组件。1
2
3
4
5
6
7
8
9
10
11
12
13
14//next()表示放行,next("url")表示拦截到url
router.beforeEach((to, from, next) => {
//前两个是对象(和$route一样 是一个反应式的对象(路由对象)),后一个是函数
//如果要访问的网站to对象的路径:path 不是'/pay', '/myorder'
if (!paths.includes(to.path)) {
next()//放行
} else {
if (store.getters.token) {
next()//如果登录了,放行
} else {
next('/login') //拦截到 登录页面
}
}
})
$route和$router的区别
一个是路由,表示当前页面的路由对象;一个是路由器,记录了所有页面的对应的路由路径。
在组件内可以通过this.$route
或得当前组件
对应的路径。
404页面
{ path: '*', component: NotFound }
,路由的匹配顺序是声明顺序,通常写在最后,匹配不到组件就匹配这个组件 。
当我们使用的路由是history路由
的时候,这个页面非常有用,因为后端为了防止使用history路由的页面,将前端路由发送到后端导致响应404,所以每当匹配不到资源的时候,后端都返回index.html
,这样浏览器就会重新解析html,渲染页面,将路由控制权交给前端路由。但是这样也有缺点,就是如果用户请求的路径,既不对应后端资源也不对应前端页面,这种情况,确实需要响应404,我们就需要一个404页面来提示用户。
区别
在vue2中
1 | //router/index.js |
在vue3中
1 | //router/index.js |
import.meta.env.BASE_URL
:路由基地址
,导入的是vite的环境变量,可以通过修改vite.config.js
文件的base属性来改变基地址,不能把import.meta.env.BASE_URL
直接替换成'./'
这种字面量。
scrollBehavior
是 Vue Router 3.5.0 版本引入的一个功能,允许你定义一个滚动行为的函数,用于在导航时控制页面的滚动位置。
它和routes
这个常见的配置项属于同级别的属性。
scrollBehavior
函数接收三个参数:
- to:即将进入的路由对象。
- from:即将离开的路由对象。
- savedPosition:仅当
popstate
导航(如用户点击浏览器的前进/后退
按钮)时可用。这是一个包含滚动位置的对象
(如果有保存的话),比如{top:20}
。savedPosition记录的是用户点击前进或后退按钮时,的页面滚动位置
1 | const router = createRouter({ |
这段代码会在用户点击浏览器的前进/后退按钮时恢复之前保存的滚动位置;如果没有保存的位置,则默认滚动到顶部。
创造实例方式
- 一个是使用
构造函数
创建router实例,一个是通过函数创造 - 使用的库都是vue-router
- 创造实例的时候传入的都是配置项
路由模式
- vue2中控制路由用
mode
属性,history标识历史模式,hash是默认模式 - vue3中控制路由模式用
history
属性,createwebHistory
表示历史模式,createwebHashHistory
表示哈希模式。
获取router/route对象方式
vue2在组件中可以通过
this.$router/$route
获取,在js文件中则通过直接导入router的方式实现路由跳转。在vue3中,在普通js文件中,也是通过直接导入router的方式实现路由跳转,在组件中(默认使用setup语法糖),通过useRoute, useRouter这两个api分别获取
当前页面路由对象
和路由器对象
,因为没有this
,所以不能再使用vue2中的语法。1
2
3
4//在js
import router from '@/router';
// 执行导航操作
router.push('/some-path');1
2
3
4
5
6<script setup>
//这些函数依赖于Vue的响应式系统和组件上下文,只能在组件内使用,在普通js文件中无法使用。
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
</script>要注意的是,在组件内使用
useRoute, useRouter
,不要在函数内部需要的时候再使用,否则api调用返回的值是undefined
。无论是在vue2还是在vue3中,在template中都能通过
$route
访问当前页面路由对象。
导航守卫
在 Vue 3 中,如果你使用的是 Vue Router 4,在某些情况下,你可以直接返回值(return或者return true)来代替调用 next()
,这使得代码更加简洁,但是也要注意这样会终止
导航守卫函数。
1 | router.beforeEach((to, from) => { |
Vuex
在vue2开发过程中使用的状态管理工具。
场景
- 某个状态在很多个组件来使用(个人信息)
- 多个组件共同维护一份数据比如(购物车)
- 数据传递存在困难用vuex就完事了
优势
- 共同维护同一份数据
- 响应式变化,响应式变化基于vue的响应式
- 操作简洁
注册
创建vuecli项目时勾选vuex或者手动添加。
- 安装vuex
- 在src新建文件store,新建文件index.js
- 在index文件中初始化插件:Vue.use(Vuex);创造空仓库,配置仓库。
- 导出store对象,最后在创建根实例的时候传入这个store对象,它最终会被注入到每个组件实例中,也就是说
this.$store
是组件实例自己的属性。
1 | import Vue from 'vue' |
1 | import Vue from 'vue' |
四大属性
state
提供唯一的公共数据源
无论组件内外,都是从store对象出发,来拿到state,不过获取store对象的方式不同。
组件内的可以通过this.$store
拿到,在模板内可以直接使用$store
(模板内默认去除this),后面的3大属性同理。
组件外则通过import导入:import store from './store'
,直接拿到store实例。
state中的数据的生命周期
初始化:当 Vuex store实例被创建时,定义在 store 中的 state 也会一同被初始化。这意味着一旦应用启动,并创建了 Vuex store 实例,state 就会被初始化并准备好使用。
页面刷新:默认情况下,Vuex 的 state 在页面刷新时,会重新初始化。这是因为 state 存储在内存中,页面刷新会导致内存中的数据丢失。如果你需要在页面刷新后保持 state 数据,可以使用一些持久化存储的方法,如 localStorage 或者 sessionStorage,初始化的时候再从中取数据。
应用运行期间:在应用运行的过程中,state 可以通过 commit mutations 来直接改变,或通过 dispatch actions 后再 commit mutations 来间接改变。这是 Vuex 管理状态变化的主要方式,确保状态的变化是可追踪和预测的。
路由变化:当用户在应用的不同路由之间导航时,除非明确改变了 state 或者触发了相关的 mutations/actions,否则 state 不会因为路由变化而自动改变。
组件销毁与重建:Vue 组件在其生命周期内,可能会被多次销毁和重建(例如,在动态路由或条件渲染下)。但是,只要 Vuex store 没有被销毁,state 将保持不变,不会因单个组件的销毁和重建受到影响。
挂载位置
模块中的state,最终会挂载在根仓库的state中,**无论模块是否开启命名空间(开启命名空间是防止命名冲突)**,但要注意的是根仓库中的属性直接通过this.$store.state
就能拿到,比如this.$store.state.index
,而模块中的属性需要通过this.$store.state.module
才能拿到,路径更长,比如购物车模块中的购物车列表,通过this.$store.state.cart.cartList
才能拿到。
例子如下:
1 | export default new Vuex.Store({ |
1 | //modules/cart.js |
1 | //modules/user.js |


可以看到user模块即便没有开启命名空间,其state中的数据也还是不会直接挂载在this.$store.state
下,而是挂载在this.$store.state.user
下。
mutations
里面是一些修改/维护state中的数据的函数,只能通过调用这里的函数来修改数据。
参数
第一个参数是state,用来访问state中的数据,也同时说明了mutations只能同步修改数据
1 | mutations: { |
我们在使用mutations中的方法的时候忽略第一个参数,我们传入的参数就是index,比如this.$store.commit('setIndex',1)
挂载位置
是否开启命名空间,也不影响mutations的挂载位置。但是会影响挂载时候的属性名,举个例子:
1 | export default new Vuex.Store({ |
1 | //modules/cart.js |
1 | //modules/user.js |

如图所示,cart模块开启了命名空间,所以属性名加上了cart/
前缀,而user模块未开启,就不会加上前缀,如同根仓库中的mutations,但是无论开不开启命名空间,它们模块的mutations中的方法都是直接挂载在this.$store._mutations
下的
然后我们使用commit调用mutations中的方法,就有:
1 | this.$store.commit(属性名,参数) |
比如:
1 | this.$store.commit('cart/changeCount',3) |
commit是Store实例的自己的属性,值是函数
getter
相当于计算属性,第一个参数也一般是state,第二个参数可以是getters(用来拿到getters中的属性)
挂载位置
是否开启命名空间,也不影响getter的挂载位置。但是会影响挂载时候的属性名,举个例子:
1 | export default new Vuex.Store({ |
1 | //modules/cart.js |
1 | //modules/user.js |

如图所示,cart模块开启了命名空间,所以属性名加上了cart/
前缀,而user模块未开启,就不会加上前缀,如同根仓库中的getter。
这一挂载规则就和mutations的挂载规则相同,因为getters本质也是函数啊。
然后我们访问的时候就有:
1 | this.$store.getters[属性名] |
比如:
1 | this.$store.getters['token'] |
actions
里面是一些异步操作/函数
参数
第一个参数是context(上下文),可以通过context.commit()
调用mutations
里的方法,来修改state,通过context.dispatch()
调用actions
里的方法;可以通过context.state
拿到数据但是不能直接修改数据,总之就是雨露均沾。
挂载位置
原理和mutations,getters
一样(三者本质都是函数啊),是否开启命名空间,也不影响actions的挂载位置,都挂载在Store._actions
属性中。
然后调用的时候就有:
1 | this.$store.dispatch('属性名',参数)//类似mutations |
dispatch
和commit
一样是Store实例的自己的属性,值是函数。
辅助函数
在我们了解了各个属性在Store实例中的挂载位置后,我们拿到Store实例后,就知道如何访问,使用这四个属性了。
其实vuex还提供了辅助函数
来简化操作。
四个属性,分别对应四个辅助函数,mapState,mapMutations,mapActions,mapGetters
辅助函数内部本质其实也是使用this.$store
来执行各种操作的,所以只能在组件中使用。
它们的使用方法是类似的。
mapState
mapState返回的是一个对象,这个对象可以有多个属性,属性的值类型都是函数,都是计算属性,内部使用了this.$store
来获取state中的数据。

1 | import { mapState } from 'vuex' |
如果使用辅助函数使用了传递了2个参数的形式,如图,则第一参数是模块名,要求必须开启命名空间。
mapMutations

1 | import { mapMutations } from 'vuex' |
mapGetters

1 | import { mapGetters } from 'vuex' |
Pinia
vue的最新状态管理工具
,vuex的替代品。
优点
和Vue3新语法统一,提供符合
组合式
风格的API提供更加简单的API(去掉了mutation,合并到actions)
去掉了
modules
的概念,替换为一个一个的同级别
的store,他们都由pinia管理,创建好的pinia实例最终会在app上注册,也就是vue实例上(app.use(pinia)
)配合TypeScript更加友好,提供可靠的类型推断。
注册
1 | //main.js |
组合式风格
1 | //cart.js |
特点:
- 定义的变量就是state属性
- 定义的计算属性。即computed,就是getters
- 定义的函数,即function(),就是actions,支持异步操作,也支持同步操作
选项式风格
个人还是一味的使用组合式了….
1 | import { defineStore } from 'pinia' |
持久化插件
无论是vuex还是pinia,仓库中的数据是保存在内存中的,如果不使用持久化存储,当页面刷新或者关闭时,数据就会丢失。这是因为这些状态是存储在 JavaScript 运行时的内存中的,一旦页面卸载(比如刷新或关闭),这些数据就会被销毁。
使用步骤
1 | npm i pinia-plugin-persistedstate |
1 | import { createPinia } from 'pinia' |
再指定要持久化的store,对于组合式store
,需要在defineStore函数里再传入一个参数(第三个参数),{persist:true}。
对于选项式store
,直接添加属性persist:true
即可
1 | export const category = defineStore('category',() => {},{persist:true}) |
原理
- 简化了localStorage的相关操作,会自动使用
JSON. stringify/JSON.parse
进行序列化/反序列化
。值为函数和undefined的属性无法被序列化,Symbol属性和值为Symbol的属性无法被序列化。 - 存储到localStorage的键名默认是
仓库唯一标识
。 - 默认把仓库整个state做持久化,可以指定具体哪些数据做持久化。
使用
1 | // 假设在stores/counter.js中定义了一个仓库 |
1 | //在组件中使用 |
router的useRoute和useRouter
只能在组件中使用,在想要在js文件中使用,就必须引入router
对象。
无论在是组件还是js文件中,都只能通过引入并调用useXXXStore
的方式获得store对象,然后才能访问其中的状态,调用其中提供的方法。不过,在js 文件(非 Vue 组件)中使用 Pinia store 需要一些额外的步骤,因为你没有 Vue 组件的上下文(比如 setup()
函数)。需要确保 Pinia 已经被安装并且可以访问到,所以最好不要写到js文件的全局作用域
中。
与vuex的区别
- vue2中要使用vuex中的哪个数据,调用哪个方法,都是单独获取,而Vue3是一个一个
store
整体导入使用。 - 在vue2的组件中,通过
this.$store
就能访问到store对象,而在js文件中就需要手动导入。
启用命名空间
在 Pinia 中,无需手动启用命名空间特性即可避免 Store 之间的命名冲突,这是其设计哲学的一部分。以下是具体实现原理和实践方法:
Pinia 通过 唯一 Store ID 自动实现命名空间隔离。每个 Store 在定义时必须指定一个全局唯一的 ID(字符串),该 ID 会作为所有状态的命名空间前缀。
1 | // 定义 user 模块 |
若两个 Store 使用相同 ID(如 defineStore('user', ...)
重复),构建时会直接报错。
Vue-cli
是什么
vue使用webpack进行模块化开发,但是webpack的配置工作是一个很繁琐的过程,所以出现了vue-cli这个脚手架工具,它可以帮助我们快速创建一个开发vue项目的标准化webpack配置。
快速开始
1 | npm i @vue/cli -g//全局安装,安装一次就行,之后就可以在任意目录执行以下指令 |
项目结构
node_modules:存储项目的所有 npm 依赖包。
public:存放
静态资源
,如 index.html 和 favicon.ico,这些文件不会被 webpack 处理,而是会直接被打包进最终文件。src:源代码的主要目录
assets:用于存放静态资源文件,如图片、字体等,会被 webpack 处理。
components :存放 .vue 单文件组件
views:存放页面
App.vue:根组件,整个应用的入口点。
main.js:应用程序的入口脚本,通常在这里创建 Vue 实例(根实例)。
1
2
3
4
5new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
package.json:项目元数据和依赖关系列表,以及 npm 脚本(script)。
vue.config.js:Vue CLI 的可选配置文件,用于自定义构建设置
babel.config.js:Babel 的配置文件,用于转换 ES6+ 代码。它可以帮助你将现代 JavaScript 特性(ES6+)转换为向后兼容的 JavaScript 版本。
json-server
是一个轻量级的 Node.js 模块,可以让你快速地启动一个具有 REST API 的 JSON 文件数据库服务器。它提供了一个简单的、可立即使用的后端。
1 | npm install -g json-server |
在项目根目录下创建一个文件,比如 db.json,并填充一些数据
1 | { |
在db.json文件所在目录下执行json-server --watch db.json
,这会启动一个默认监听在http://localhost:3000
的服务器
使用各种 HTTP 请求工具
(如 Postman 或 cURL)或前端框架中的 AJAX 请求来与之交互,比如可以发送一个 GET 请求到http://localhost:3000/posts
来获取所有的帖子
eslint,eslint扩展,prettier
eslint扩展
ESLint 扩展的作用是在编码的时候,就提示代码存在的问题,通常依赖于项目中的本地安装的 ESLint。这意味着它会在项目的node_modules
目录中寻找 ESLint。如果您没有在项目中安装 ESLint,VSCode 的 ESLint 扩展可能会无法正常工作。
eslint和prettier
我们在创建vue3项目的时候,就能根据vue-create脚手架的提示,快速的下载好eslint和prettier所需的包
1 | "devDependencies": { |
然后目录下还会多出.eslintrc.cjs
和.prettierrc.json
文件,.eslintrc.cjs
是ESLint 的配置文件,主要用于代码质量检查和部分风格约束(如变量未声明、未使用的导入等);.prettierrc.json
是Prettier 的配置文件,专注于代码格式化(如缩进、引号、换行符等),不涉及代码逻辑问题。
1 | //.eslintrc.cjs |
1 | //.prettierrc.json |
同时项目根目录下的.vscode
文件中也会多出一个setting.json
文件(相比于不使用eslint和prettier的项目),其实也不需要我们过于操心这个文件,一般保持默认配置就好。
1 | //setting.json |
保存文件时,第一步,Prettier 自动格式化代码(formatOnSave),第二步,用户手动触发代码修复(codeActionsOnSave:explicit)
此时,我们编码的时候,vscode并不会提示我们代码不符合prettier规范,因为我们没安装prettier扩展
。
但是我们安装了eslint扩展
啊,只要把prettier的配置代码集成到eslint的配置代码中就好了,这样编写代码的时候,也会有prettier规范提示。
1 | /* eslint-env node */ |
如果发现修改.eslintrc.cjs
保存后没有效果,则可以尝试重启vscode,重新加载这个配置文件。
husky
是什么
husky能够帮助我们轻松的设置git hooks。
Husky 将 Git 原生钩子(如 pre-commit
)的配置,从 .git/hooks
迁移到项目根目录的 .husky
文件夹中,通过脚本化的方式统一管理,避免钩子文件被 Git 忽略或覆盖的问题。
husky通常与lint-staged
配套使用,lint-staged 是一个针对 Git 暂存区(Staged Files)运行代码检查(Linters)和格式化工具的工具。它通过仅对即将提交的代码,进行增量检查,避免全量扫描,从而显著提升校验效率。例如,在提交前仅对修改过的 .js
文件运行 ESLint,而不是整个项目。
lint-staged 本身不依赖 ESLint,但通常与 ESLint 结合使用。其核心功能是按需调用不同的代码检查工具,具体支持的 Linter 包括:ESLint,Prettier,Stylelint,TSLint(TypeScript,已逐渐被 ESLint 替代)
使用步骤
安装:
1 | npm install husky lint-staged --save-dev |
初始化:要注意的是,再初始化之前,需要先确保git仓库
存在,Husky 的核心功能是管理 Git 钩子(Git Hooks),而 Git 钩子本身是 Git 仓库的组成部分。只有当项目是 Git 仓库时,Husky 才能通过修改 Git 配置(如 core.hookspath
)将钩子脚本的存储路径从 .git/hooks
重定向到 .husky
目录。
1 | npx husky init # v9+版本需执行此命令生成.husky目录[7,9](@ref) |
我们执行这个初始化命令后,发现根目录下多出来一个名为.husky
的文件夹,.husky/_
目录下通常包含 husky.sh
等脚本文件,用于统一管理 Git 钩子的执行环境,_
目录内容由 Husky 自动维护,手动修改可能导致钩子功能异常,简单的来说,不需要我们管理,操心。
配置钩子脚本:在pre-commit
文件中书写:
1 | npx lint-staged |
然后再我们提交代码之前,这个命令就会被执行
与代码检查工具集成:在 package.json
中配置 lint-staged
,这样我们才知道执行npx lint-staged
,具体做了什么
1 | { |
eslint --fix
:使用 ESLint 自动修复可修复的代码问题(如语法错误、风格问题),这行命令在下载好eslint后是可以直接执行的
prettier --write
:使用 Prettier 对文件进行格式化(如缩进、引号、换行符等),这行命令在下载好prettier后是可以直接执行的
vetur
Vetur插件是一个为Vue.js
项目提供支持的Visual Studio Code(VS Code)插件,适用于vue2。
功能
- 语法高亮:Vetur插件为Vue.js文件提供了语法高亮,让你的代码更易于阅读和理解
- 智能补全:当你输入Vue组件或属性时,它会自动提示可用选项,并提供文档信息
- 代码导航:Vetur插件可以帮助你更轻松地导航和理解Vue.js项目的结构。你可以点击组件名称或引用,快速跳转到相关的文件。
- 语法检查:Vetur插件集成了ESLint和TSLint,可以帮助你在编码时捕获潜在的错误和不规范的代码风格。
问题
Vetur的语法检查会认为vue3的一些新特性,比如可以有两个根元素,v-model:xxx=' xxx'
为语法错误。
解决办法:打开设置,搜索 Vetur › Validation: Template ,关闭语法检查,本质上修改扩展设置。
组件样式渗透
scoped
只会给当前组件内的标签元素,添加唯一的data-v-hash
属性,并给当前组件内的所有选择器加上属性选择。
父组件通过插槽传递给子组件的内容(即父组件模板中直接编写的插槽内容),会被父组件的scoped
样式处理,添加父组件的data-v-hash
属性;而子组件内部模板中的内容(包括默认插槽或具名插槽),则由子组件自身的scoped
处理,添加子组件的data-v-hash
属性。
1 | <!-- 父组件模板 --> |
编译后,父组件的<div class="slot-content">
会被添加父组件的data-v-xxx
属性,如:<div class="slot-content" data-v-xxx>
。若子组件自身模板中包含插槽(如<slot>
),且存在默认结构,则这部分内容由子组件的scoped
处理,添加子组件的data-v-yyy
属性。
默认情况下,父组件的scoped
,样式不会影响子组件内部的元素(包括slot),因为子组件可能有自己的data-v-yyy
属性。
如果在当前组件内使用了scoped
的情况下,想要修改子组件内的样式,就可以使用样式渗透,下面举个项目中的例子
1 | <div class="editor"> |
1 | .editor { |
显然,quill-editor
,这个富文本组件中存在类名为ql-editor
的标签,但是因为我们在组件中开启了scoped
,所以不能直接使用:
1 | .editor { |
来修改quill-editor
组件中的元素,因为编译后的样式会变为:
1 | .editor[data-v-8a25066b] .ql-editor[data-v-8a25066b] { |
但是这个富文本组件中,类名为ql-editor
的标签并没有添加任何属性,所以,这个样式不会生效。
但是如果我们使用了样式渗透,编译后的样式代码就会变为:
1 | .editor[data-v-8a25066b] .ql-editor { |
显然,这样是能够生效的。由此我们可以看出,使用样式渗透,就是为了解决因为组件内开启了scoped,导致无法直接修改子组件内元素样式的问题。
然而,样式渗透的语法分别是如何的呢?
原生 CSS:>>>
直接作用于子组件内部元素,但部分预处理器可能不支持:
1 | /* 父组件样式 */ |
编译后:.parent[data-v-xxx] .child-inner
适用场景:原生 CSS 项目
预处理器兼容语法:/deep/
通用性更强,支持 SASS/LESS 等:
1 | .parent /deep/ .child-inner { |
编译后:.parent[data-v-xxx] .child-inner
注意:在 Vue3 中已废弃
Vue2 专用语法:::v-deep
与 /deep/
等价,但语义更明确:
1 | .parent::v-deep .child-inner { |
我们观察到::v-deep
是附加在.parent
后面的,这表明了.parent
就是父组件中的元素,而 .child-inner
是子组件中的目标元素
编译后:.parent[data-v-xxx] .child-inner
适用场景:Vue2 项目升级过渡期
Vue3 样式穿透语法
Vue3 统一使用 CSS 伪类 :deep()
,废弃了 >>>
和 /deep/
1 | /* 修改子组件内部的 .child-inner 样式 */ |
编译后:.parent[data-v-xxx] .child-inner
优势:
- 语法标准化,符合 CSS 规范
- 支持嵌套和动态选择器
富文本编辑器VueQuill
是什么
VueQuill 是基于 Quill.js 的 Vue 3 专用富文本编辑器组件,它将 Quill.js 的功能封装为 Vue 的响应式组件,提供与 Vue 生态无缝集成的开发体验。Quill.js 本身以轻量、模块化和高扩展性著称,而 VueQuill 在此基础上进一步优化了配置方式和 API 设计。
特点如下:
- 支持
v-model
双向绑定,直接通过content
属性管理富文本数据 - 响应式更新机制与 Vue 的生命周期完美契合,便于状态管理
示例:
1 | <template> |
- 可通过
options.modules.toolbar
配置工具栏按钮(如加粗、列表、图片上传等) - 支持按需加载功能模块(如代码高亮、表格插入),减少打包体积
示例:
1 | const editorOptions = { |
使用步骤
安装依赖:
1
npm install @vueup/vue-quill quill --save
全局或局部引入组件:
全局注册:
1
2
3import { QuillEditor } from '@vueup/vue-quill';//引入的是一个组件
import '@vueup/vue-quill/dist/vue-quill.snow.css';
app.component('QuillEditor', QuillEditor);局部引入:
1
2
3
4<script setup>
import '@vueup/vue-quill/dist/vue-quill.snow.css';
import { QuillEditor } from '@vueup/vue-quill';
</script>在模板中使用:
1
2
3
4
5
6
7
8
9
10<div class="editor">
<!-- 通过ref拿到quillEditor组件实例 -->
<quill-editor
ref="editorRef"
v-model:content="formModel.content"
theme="snow"
content-type="html"
>
</quill-editor>
</div>常用api:
editorRef.value.setHTML('')
,清空编辑器的内容