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

Vue2
前端发展背景与vue
发展背景
最早的网页是没有数据库的,可以理解成就是一张可以在网络上浏览的报纸,就是纯静态页面
直到CGI
技术的出现,通过 CGI Perl 运行一小段代码,与数据库或文件系统进行交互
(前后端交互)
后来JSP(Java Server Pages)技术取代了CGI技术,其实就是Java + HTML
1 | <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> |
JSP有一个很大的缺点,就是不太灵活。JSP使用 Java
而不是 JavaScript
,并且 Java 代码只能在服务器端运行。我们每次的请求:获取的数据、内容的加载,服务器都会做对应的处理,并渲染dom然后返回渲染好的dom,简单的来说,JSP把页面的渲染工作完全交给后端服务器。
后来ajax
火了,它允许用户在不刷新整个页面的前提下,和后端服务器交换数据,并由浏览器执行js代码,更新部分页面。
随后移动设备的普及,Jquery的出现,以及SPA(Single Page Application 单页面应用)的雏形,Backbone EmberJS,AngularJS 这样一批前端框架随之出现,但当时SPA的路不好走,例如SEO问题,SPA 过多的页面、复杂场景下 View 的绑定等,都没有很好的处理。
经过这几年的飞速发展,节约了开发人员大量的精力、降低了开发者和开发过程的门槛,极大提升了开发效率和迭代速度。我们可以看到Web技术的变化之大与快,每一种新的技术出现都是一些特定场景的解决方案,那我们今天的主角Vue又是为了解决什么呢?
Vue是什么
是一个用于创建用户界面
的开源JavaScript框架
,也是一个创建单页应用(SPA)
的前端框架。
Vue核心特性
数据驱动视图更新
数据驱动(MVVM),相比于react,开发者无需手动调用
setState
等api来提示视图更新。组件化
降低了代码的耦合度,提高了代码的可维护性,可复用性,便于调试。vue中的组件可分为单文件组件和多文件组件,vue中的组件是能实现部分功能的
css,js,html
等代码和资源的集合。指令系统
指令 (Directives) 是带有
v- 前缀
的特殊属性
,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。简单的来说,vue中的指令系统简化了dom操作,而react中没有指令系统。
插值表达式
利用表达式进行插值,渲染数据到页面中
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"> |
v-for
指令中的参数比如(value,key)
,能被该标签内的其他属性使用,还能在标签体内使用。Vue 的模板语法中,标签的属性和内容共享同一个作用域,v-for
中定义的参数(如 value
和 key
)会被自动注入到当前标签及其子节点的作用域中。这意味着,只要是在这个作用域内的代码(标签属性、子元素、插值表达式等),都可以访问这些参数。
关于给标签添加key
的作用,还有v-if
和v-for
优先级问题以及能否一起使用的问题,参考前端面试—vue部分 | 三叶的博客
感兴趣的还能去了解一下v-for
指令的实现方式。
v-model
给表单元素使用,双向数据绑定,可以快速获取或设置表单元素内容。
1 | <input type="text" v-model="a"> |
本质上是一个语法糖:
1 | <input type="text" :value="a" @input="(e)=>{a = e.target.value}"> |
如果a是一个非响应式的普通对象,当修改输入的值的时候,a的值确实会被改变,input的视图也会更新,但是这是浏览器原生行为:用户输入直接改变了 DOM 元素的
.value
属性,但是a的值改变了这件事没人知道,所以模板中,其他部分使用了a的地方的视图不会更新。如果a是一个非响应式的普通对象,当我们直接修改a的时候,input的视图也不会更新,因为不会重新把a的值赋给
input.value
应用于其他表单元素
checkbox:和checked属性双向绑定
1
<input type="checkbox" :checked="a" @input="(e)=>{a = e.target.checked}">
radio:应该也是和value属性双向绑定吧。
给同一组radio绑定相同的变量,点击哪个就会把哪个的value赋值给变量
1
2<input v-model="gender" type="radio" name="gender" value="1">男
<input v-model="gender" type="radio" name="gender" value="2">女
给组件使用v-model
vue2中的sync
1 | <!-- 父组件 --> |
sync
修饰符,通常与v-bind
指令一起使用,用来简化子组件向父组件通信的代码:
- 不需要给自定义事件取名
- 不需要在子组件上添加事件监听
- 不需要书写对应的回调函数,回调函数默认是修改动态传入的值
因为在vue2中直接给组件使用v-model,就意味着只能传入value属性,监听的事件只能是input,而借助sync,就能指定多个传入子组件的属性和对应的事件。所以说sync + v-bind
就是用来解决vue2中,v-model应用在组件上功能不足的问题。
vue3中的v-model:xxx
在 Vue 3 中,.sync
修饰符已经被移除,推荐的做法是使用自定义事件(就是手动给组件标签添加事件监听并传入事件回调)或 来达到类似的效果。vue 3 对
v-model
进行了增强,使其更加灵活,允许在一个组件上使用多个 v-model
绑定,并且可以自定义绑定的prop
和 event
名称。
1 | <ChildComponent v-model="message" /> |
在vue3中等价于:
1 | <ChildComponent :modelValue="message" @update:modelValue="(val)=>{message=val}" /> |
在vue2中等价于:
1 | <ChildComponent :value="message" @input="(val) => { message=val }" /> |
再举个例子,这是vue3中独有的写法
1 | <ChildComponent v-model:title="pageTitle" /> |
在vue3中等价于:
1 | <ChildComponent :title="pageTitle" @update:title="pageDescription" /> |
要注意的是,给组件添加事件监听和给dom元素添加事件监听,都传入一个回调函数,但不同的是,Vue 组件中的事件是自定义事件,不是浏览器原生的 DOM 事件,没有与之相关的 event(事件)
对象,对于组件的自定义事件的回调函数,传入的第一个参数其实是子组件通过emit传递过来的值,后者传入的则是事件对象event。
指令修饰符
计算属性computed
基于现有的数据,计算出来的新属性。依赖的数据变化,自动重新计算。在模板中使用起来和普通数据一样: {{计算属性名}}
计算属性有很多种写法
1 | computed: { |
计算属性一般只用来展示,赋值。如果尝试直接修改计算属性,并不会生效,因为计算属性的值只与其相关的数据有关,但是会把传入的值传递到set函数,set函数拿到这个值可以做一些操作。
依赖的数据必须是响应式的(即 data
或其他计算属性),不然依赖的数据改变了计算属性也无法发觉,就不会即时更新。
当计算属性依赖的任何数据发生变化时,Vue 会标记计算属性为脏
,并在下次访问时重新计算其值。计算属性采用惰性求值策略(被访问的时候再求值),并具有缓存机制(如果依赖的数据未改变,直接使用缓存的值,而不需要重新计算),只有当依赖的数据发生变化,即被标记为脏,并被访问的时候,才会重新计算其值。
同时计算属性也是响应式的,当计算属性的值改变,也会通知计算属性的依赖更新。
计算属性在vue2,vue3中的实现方式不同,想深入了解可以参考面试系列文章,了解实现原理能帮助我们深入了解计算属性。
监听器watch
用来监视data
和计算属性
中数据的变化。
1 | watch:{ |
deep: true
:开启对复杂类型深度监视后,可以监听一整个对象,可以监听这个对象中的全部属性,否则监听的只是对象的地址变化。
immediate:true
:如此配置后,传入的cb会在watch注册后立即执行。
通过vue实例的$watch
方法也能添加监听
1 | vue.$watch('监听的数据',{//配置对象})//完整写法 |
watch监听的数据必须是响应式的,然而在vue2中数据默认都是响应式的。计算属性和watch都是基于响应式机制实现的。
同样的watch在vue2,vue3中的实现方式不同,了解实现原理能帮助我们深入了解watch
Vue3
setup()
- vue3中的一个新的配置项,值为一个函数,组合式api都写在这里面。
- 执行时机在
beforeCreate()
之前;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标签,不需要return |
具体来说,<script setup>
中的代码会在组件实例被创建时执行一次,这相当于传统选项式 API 中 beforeCreate
和 created
生命周期钩子之间的某个时间点。这是因为 <script setup>
中的逻辑,会被包裹进一个自动生成的 setup()
函数内,这个函数会在组件实例创建时被调用一次。
setup的参数
setup(props, context)
,setup的参数在混合选项式api的时候,也就当不使用setup语法糖开发的时候是有意义的,使用语法糖开发的时候参数都没了。
props
是第一个参数,值为对象,包含组件外部传入组件的,且在组件内部接收的值,也就是在props属性
中接受的值。
这个参数的作用就在于,让通过选项式api中的props属性接收的值,能够在setup函数内部使用。
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(value) { |
1 | class RefImpl { |
1 | function toRaw(value){ |
我们可以观察到,构造函数中并没有初始化value
,但是refImpl
实例中出现了,我自己测试了一下,发现并没有借助defineProperty
1 | //自定义一个refImpl类 |
最后打印p的结构如图:

观察到,value属性出现了,这就说明,是通过在原型上挂载get value()
和 set value(val)
定义了value属性。
创建ref对象
当我们给ref()
传入一个基本类型的数据,返回的refImpl
中:
_rawValue
属性:默认情况this._rawValue = toRaw(value)
,由于value是基本数据类型,所以直接把value赋给_rawValue
属性_value
属性:默认情况this._value = toReactive(value)
,由于value是基本数据类型,所以直接把value赋给_value
属性

当我们给ref()
传入一个普通的js对象,返回的refImpl
中:
_rawValue
属性:默认情况this._rawValue = toRaw(value)
,由于普通对象就是原始类型,所以直接把这个对象赋给this._rawValue
_value
属性:默认情况this._value = toReactive(value)
,由于value(用户传入的对象)是个普通的对象,所以会调用reactive()
方法包装对象,返回一个proxy对象,再赋给this._value

当我们给ref()
传入一个refImpl
对象,会直接返回。
当我们传入一个proxy对象:
_rawValue
属性:默认情况this._rawValue = toRaw(value)
,如果是proxy
对象,则将proxy.__v_raw
赋给this._rawValue
_value
属性:默认情况this._value = toReactive(value)
,如果value是proxy
,直接赋给this._value
1
2
3
4import { reactive, ref } from "vue";
const obj = reactive({ a: 1, b: 2 });
const refValue = ref(obj);
console.log(obj === refValue.value); //输出true添加
get value(),set value()
,劫持value
属性,并添加依赖收集,触发依赖更新逻辑,从而实现给value属性添加响应式
修改ref的value
1 | set value(newVal) { |
当newVal是基本数据类型,则
_rawValue
和_value
都被修改为这个基本数据类型;当newValue是一个普通的对象,则
_rawValue
被修改为这个对象,_value
被修改为这个reactive包装的对象;如果newVal是一个proxy对象,则
_rawValue
被修改为这个对象的__v_raw
属性,_value
被修改为这个proxy对象。
toRef()和toRefs()
响应式丢失
1 | const obj = reactive({ foo:1, bar:2 }) |
在上述例子中,我们打印newObject,输出一个普通的对象{ foo:1, bar:2 }
,这个对象并不具备响应式,当然也可以自己测试一下。
使用扩展运算符进行对象的浅拷贝,其实也只是将一个个属性拷贝然后读取对应的值。
toRef()
如何来解决这个问题呢?我们可以借鉴ref给基本数据类型添加响应式的方案。
1 | const obj = reactive({ foo:1, bar:2 }) |
可以看到toRef返回了一个新的对象newObj,这个对象有一个访问器属性value,当访问newObj的value属性其实是在访问obj的foo属性,由于obj是响应式数据,所以newObj.value
会触发依赖收集(副作用收集)。当修改newObj.value
其实是在修改obj的foo属性,所以能触发依赖更新。
toRefs()
使用toRefs
可以将一个对象上的第一层属性都转化为ref类型
1 | const obj = reactive({ foo:1, bar:2 }) |
computed
1 | import { computed } from 'vue' |
vue3中的computed属性默认是响应式的
避免直接修改计算属性的值,计算属性应该是只读的,特殊情况可以配置get,set
计算属性中不应该有
副作用
,不建议在计算属性中写dom操作和异步请求
与vue2计算属性的语法区别:
- vue2使用computed不需要导入,是一个配置属性;vue3中的computed是一个函数,需要按需导入。
- vue2中
批量添加
计算属性更方便,在vue3中每次获得一个计算属性都要传入一个回调函数,调用一次computed
函数
watch
监听单个 ref
对象:
1 | const count = ref(0) |
监听多个 ref
或响应式数据(数组形式):
1 | const count = ref(0) |
监听reactive对象的单个属性
1 | const userInfo = reactive({ |
监听reactive对象,默认深度监听
1 | import { reactive, watch, ref } from "vue"; |
停止监听:
1 | const stopWatch = watch(count, () => { |
与vue2中watch的语法区别
- vue2中watch是属性,vue3中是需要导入的函数
- 在vue2的watch中的函数名就是监听的对象,cb是函数体
- vue2中添加多个监听对象只需要都写在watch:{}中就行,vue3中可以放在数组中整体监听
生命周期函数
在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'; |
模板引用
步骤
- 调用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
的作用是 主动阻止默认行为,二者语义冲突,所以不能同时使用。