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

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

Vue2

前端发展背景与vue

发展背景

最早的网页是没有数据库的,可以理解成就是一张可以在网络上浏览的报纸,就是纯静态页面

直到CGI技术的出现,通过 CGI Perl 运行一小段代码,与数据库或文件系统进行交互(前后端交互)

后来JSP(Java Server Pages)技术取代了CGI技术,其实就是Java + HTML

1
2
3
4
5
6
7
8
9
10
11
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JSP demo</title>
</head>
<body>
<img src="http://localhost:8080/web05_session/1.jpg" width="200" height="100" alt="示例图片" />
</body>
</html>

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
2
3
<h3>{{ title }}</h3>
<p>{{nickname.toUppercase()}}</p>
<p>{{age >= 18 ?'成年’:'未成年'}}</p>

注意:不能写到标签内部,表达式涉及到的数据必须存在。

指令

带有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
2
3
v-on:事件名 = "内联语句"//js代码
v-on:事件名 = "methods中的函数名"
@事件名="" //简写

v-bind

动态的设置标签属性src url title …

1
2
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
2
3
<li v-for="(value, key, index) in obj">
Key: {{ key }}, Value: {{ value }}, Index: {{ index }}
</li>//只有一个变量就是value,两个第二个就是key,三个第三个就是index

v-for指令中的参数比如(value,key)能被该标签内的其他属性使用,还能在标签体内使用。Vue 的模板语法中,标签的属性和内容共享同一个作用域,v-for 中定义的参数(如 valuekey)会被自动注入到当前标签及其子节点的作用域中。这意味着,只要是在这个作用域内的代码(标签属性、子元素、插值表达式等),都可以访问这些参数。

关于给标签添加key的作用,还有v-ifv-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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 父组件 -->
<template>
<ChildComponent :message.sync="parentMessage"/>
</template>

<!-- 子组件 -->
<script>
export default {
// ...
props:['message']
methods: {
updateMessage() {
this.$emit('update:message', newValue);
}
}
}
</script>

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 绑定,并且可以自定义绑定的propevent 名称。

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。

指令修饰符

参考前端面试—vue部分 | 三叶的博客

计算属性computed

基于现有的数据,计算出来的新属性。依赖的数据变化,自动重新计算。在模板中使用起来和普通数据一样: {{计算属性名}}

计算属性有很多种写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
computed: {
计算属性名(){
//基于现有数据,编写求值逻辑
return 结果
}
}
computed: {
计算属性名:{
get() {
//一段代码逻辑(计算逻辑)
return 结果
},
set(修改的值){
//一段代码逻辑(修改逻辑)
}
}
}

计算属性一般只用来展示,赋值。如果尝试直接修改计算属性,并不会生效,因为计算属性的值只与其相关的数据有关,但是会把传入的值传递到set函数,set函数拿到这个值可以做一些操作。

依赖的数据必须是响应式的(即 data 或其他计算属性),不然依赖的数据改变了计算属性也无法发觉,就不会即时更新。

当计算属性依赖的任何数据发生变化时,Vue 会标记计算属性为,并在下次访问时重新计算其值。计算属性采用惰性求值策略(被访问的时候再求值),并具有缓存机制(如果依赖的数据未改变,直接使用缓存的值,而不需要重新计算),只有当依赖的数据发生变化,即被标记为脏,并被访问的时候,才会重新计算其值。

同时计算属性也是响应式的,当计算属性的值改变,也会通知计算属性的依赖更新。

计算属性在vue2,vue3中的实现方式不同,想深入了解可以参考面试系列文章,了解实现原理能帮助我们深入了解计算属性。

监听器watch

用来监视data计算属性中数据的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
watch:{
//检测words数据变化
words(newValue,oldvalue) {
console.log('变化了',newValue,oldvalue)
}
//检测obj中的words数据变化
"obj.words"(newValue,oldvalue){
console.log('变化了',newValue,oldvalue)
}
}//和计算属性配置一样,都是直接放函数、
watch: {
words:{
immediate: true//初始化立刻执行一次handler方法
deep: true,//深度监视
handler(newValue,oldValue){
console.log(newValue)
}
}
}

deep: true:开启对复杂类型深度监视后,可以监听一整个对象,可以监听这个对象中的全部属性,否则监听的只是对象的地址变化。

immediate:true:如此配置后,传入的cb会在watch注册后立即执行。

通过vue实例的$watch方法也能添加监听

1
2
vue.$watch('监听的数据',{//配置对象})//完整写法
vue.$watch('监听的数据',function(newValue,oldValue){}) //简写

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,还能配置datamethods,同时在methods里面也能读取到setup中的配置,当data中和setup中存在数据冲突,setup中的数据优先级更高,但是还是建议vue2的配置和vue3的配置不要混用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
export default{
setup(){
const message = 'hello Vue3'
const logMessage = () =>{console.log(message)}
return {message,logMessage}
}
data(){
return {name:'tom'}
}
methods:{
test(){
console.log(this.message)
console.log(this.logMessage)//都可以访问到
}
}
beforeCreate() {
console.log('beforeCreate函数')
}
}
</script>

其实还能返回一个渲染函数,不过用的很少,会和模板解析后的渲染函数冲突。

语法糖写法

1
2
3
4
5
//独占一个script标签,不需要return
<script setup>
const message = 'this is a message'
const logMessage = ()=>{ console.log(message) }
</script>

具体来说,<script setup> 中的代码会在组件实例被创建时执行一次,这相当于传统选项式 API 中 beforeCreatecreated 生命周期钩子之间的某个时间点。这是因为 <script setup> 中的逻辑,会被包裹进一个自动生成的 setup() 函数内,这个函数会在组件实例创建时被调用一次。

setup的参数

setup(props, context),setup的参数在混合选项式api的时候,也就当不使用setup语法糖开发的时候是有意义的,使用语法糖开发的时候参数都没了。

props

是第一个参数,值为对象,包含组件外部传入组件的,且在组件内部接收的值,也就是在props属性中接受的值。

这个参数的作用就在于,让通过选项式api中的props属性接收的值,能够在setup函数内部使用。

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>
<h2>姓名:{{person.name}}</h2>
<h2>年龄:{{person.age}}</h2>
</template>
<script>
import { reactive } from 'vue
export default {
name: 'Demo'
props:['msg','school'],
setup(props){
console.log('---setup---', props)//输出Proxy{msg:'你好啊',school:'南昌大学'}
//数据
let person = reactive({
name:'张三'
age:18
})
//返回一个对象(常用)
return {person}
}
}
</script>

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.emitthis.$slot可以被替换为context.slots

reactive和ref

vue3中数据默认不是响应式的,需要手动添加响应式。

reactive

基本语法

  • 接受对象类型数据的参数,并返回一个响应式的对象,就是Proxy类型的对象。
  • 传入一个源对象,经过proxy操作返回一个代理对象,修改代理对象会映射到源对象。
1
2
3
4
5
<script setup>
import { reactive } from 'vue'
//执行函数传入参数变量接收
const state = reactive(obj)
</script>

简要源码

修改代理对象,会映射到源对象这一点,在传入的第一个参数:配置对象上就能看出,操作对象一直是target。

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
function reactive(target) {
// 如果目标已经是响应式的,则直接返回,如果一个对象是proxy的实例,说明它就是响应式的
if (isReactive(target)) {
return target;
}
//如果传入的对象不是响应式的,则返回一个proxy对象
return new Proxy(target, {//第一个参数传入target目标对象
//target是原对象,key是属性,receiver是代理对象
get(target, key, receiver) {
//收集依赖
track(target, key);
const result = Reflect.get(target, key, receiver);
//这说明proxy类型的响应式数据,是在被取出的时候,才递归的添加响应式的,这种方式按需递归添加响应式
return isObject(result) ? reactive(result) : result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
//通知依赖更新
trigger(target, key);
}
return result;
},
//其实也就只多了一个常用的配置函数, deleteProperty
deleteProperty(target, key) {
const hadKey = hasOwn(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey) {
//通知依赖更新
trigger(target, key);
}
return result;
}
});
}

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
2
3
4
5
function ref(value) {
// 如果 value 已经是 ref,直接返回,避免包装
// isRef(value) 判断依据就是 value.__v_isRef === true
return isRef(value) ? value : new RefImpl(value);
}
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
class RefImpl {
//下面的代码只讨论默认情况__v_isShallow = false,也就是深度响应式的情况
constructor(value, __v_isShallow = false) {
//在 Vue 的响应式系统中,它帮助追踪哪些地方使用了这个 ref,从而在数据变化时能够通知到这些地方进行更新。
this.dep = void 0;//初始值设为 void 0 即 undefined,意味着在初始化时还未开始依赖收集。
this.__v_isRef = true;//明确标识该实例是一个 ref 对象,这对于 Vue 内部识别和处理非常关键

// 存储原始值(未被包装成响应式的值)。
// 以确保即使是已包装的响应式对象,也能获取到其原始值(比如访问refImpl.rawValue)。
this._rawValue = toRaw(value);

//toReactive方法内部很会判断value是不是对象,如果是就会调用reactive()包装。由此也可以看出ref和reactive之间的联系
this._value = toReactive(value);//默认取第二个值
}

//没想到吧,在类里面还能这样定义方法
get value() {
trackRefValue(this);//依赖收集
return this._value;//返回的是_value,也就是说其实value取的值就是_value
}
set value(newVal) {
newVal = toRaw(newVal); //新的原始值
if (hasChanged(newVal, this._rawValue)) { //判断新旧值是否发生了变化
const oldVal = this._rawValue;
this._rawValue = newVal;
this._value = toReactive(newVal); //默认情况下是这样的
triggerRefValue(this, 4, newVal, oldVal);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
function toRaw(value){
if(value instanceof Proxy){
return value.__v_raw
}else{
return value
}
}
function toReactive(value) {
//如果不是对象直接返回
if (!isObject(value)) return value;
// 如果 value 已经是 reactive 的 proxy,直接返回
return reactive(value); // reactive(proxy) → 返回原 proxy
}

我们可以观察到,构造函数中并没有初始化value,但是refImpl实例中出现了,我自己测试了一下,发现并没有借助defineProperty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//自定义一个refImpl类
class refImpl {
constructor(value) {
this._value = value
}
get value() {
return this._value
}
set value(val) {
this._value = val
return 0
}
}
const p = new refImpl(123)

最后打印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
    4
    import { 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
2
3
4
5
6
7
8
9
set value(newVal) {
newVal = toRaw(newVal); //新的原始值
if (hasChanged(newVal, this._rawValue)) { //判断新旧值是否发生了变化
const oldVal = this._rawValue;
this._rawValue = newVal;
this._value = toReactive(newVal);
triggerRefValue(this, 4, newVal, oldVal);
}
}
  • 当newVal是基本数据类型,则_rawValue_value都被修改为这个基本数据类型;

  • 当newValue是一个普通的对象,则_rawValue被修改为这个对象,_value被修改为这个reactive包装的对象;

  • 如果newVal是一个proxy对象,则_rawValue被修改为这个对象的__v_raw属性,_value被修改为这个proxy对象。

toRef()和toRefs()

响应式丢失

1
2
3
const obj = reactive({ foo:1, bar:2 })
const newObject = {...obj}
console.log(newObject)

在上述例子中,我们打印newObject,输出一个普通的对象{ foo:1, bar:2 },这个对象并不具备响应式,当然也可以自己测试一下。

使用扩展运算符进行对象的浅拷贝,其实也只是将一个个属性拷贝然后读取对应的值。

toRef()

如何来解决这个问题呢?我们可以借鉴ref给基本数据类型添加响应式的方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const obj = reactive({ foo:1, bar:2 })
function toRef(target, key) {
const wrapper = {
get value() {
return target[key]
},
set value(value) {
target[key] = value
}
}
Object.defineProperty(wrapper, '__v_isRef', {
value: true
})
return wrapper
}
const newObj = toRef(obj, 'foo')

可以看到toRef返回了一个新的对象newObj,这个对象有一个访问器属性value,当访问newObj的value属性其实是在访问obj的foo属性,由于obj是响应式数据,所以newObj.value会触发依赖收集(副作用收集)。当修改newObj.value其实是在修改obj的foo属性,所以能触发依赖更新。

toRefs()

使用toRefs可以将一个对象上的第一层属性都转化为ref类型

1
2
3
4
5
6
7
8
9
10
const obj = reactive({ foo:1, bar:2 })
function toRefs(target) {
const obj = {}
for (key in target) {
obj[key] = toRef(target, key)
}
return obj
}
const refs = toRefs(obj) //会返回一个新的对象,这个对象即便使用对象解构,响应式也不会丢失
const newObj = { ...refs }//newObj中的每个属性都具有响应式

computed

1
2
3
4
import  { computed } from 'vue'
const a = computed(()=>{
return val
})
  • vue3中的computed属性默认是响应式的

  • 避免直接修改计算属性的值,计算属性应该是只读的,特殊情况可以配置get,set

  • 计算属性中不应该有副作用,不建议在计算属性中写dom操作和异步请求

与vue2计算属性的语法区别:

  • vue2使用computed不需要导入,是一个配置属性;vue3中的computed是一个函数,需要按需导入。
  • vue2中批量添加计算属性更方便,在vue3中每次获得一个计算属性都要传入一个回调函数,调用一次computed函数

watch

监听单个 ref 对象:

1
2
3
4
5
6
7
8
const count = ref(0)

watch(count, (newValue, oldValue) => {
console.log('count changed:', oldValue, '->', newValue)
})

// 修改 count 时触发
count.value++ // 输出: count changed: 0 -> 1

监听多个 ref 或响应式数据(数组形式):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const count = ref(0)
const name = ref('vue')

watch(
[count, name],
([newCount, newName], [oldCount, oldName]) => {
console.log('count 或 name 变化了')
console.log('count:', oldCount, '->', newCount)
console.log('name:', oldName, '->', newName)
}
)

count.value++ // 触发
name.value = 'Vue3' // 触发

监听reactive对象的单个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const userInfo = reactive({
age: 20,
name: 'Alice'
})

// 使用 getter 函数
watch(
() => userInfo.age,
(newVal, oldVal) => {
console.log('age changed:', oldVal, '->', newVal)
}
)

userInfo.age++ // 触发

监听reactive对象,默认深度监听

1
2
3
4
5
6
7
8
import { reactive, watch, ref } from "vue";
const obj = reactive({ a: 1, b: 2, c: { value: 3 } });

watch(obj, () => {
console.log("watch");
});
obj.a++ //输出watch
obj.c.value++ //输出"watch"

停止监听:

1
2
3
4
5
const stopWatch = watch(count, () => {
console.log('count changed')
})
// 停止监听
stopWatch()

与vue2中watch的语法区别

  • vue2中watch是属性,vue3中是需要导入的函数
  • 在vue2的watch中的函数名就是监听的对象,cb是函数体
  • vue2中添加多个监听对象只需要都写在watch:{}中就行,vue3中可以放在数组中整体监听

生命周期函数

在vue3中,既支持选项式生命周期函数,也支持组合式生命周期函数,但是要注意的是,在vue3中即便能使用选项式风格的生命周期函数,也与vue2中的不完全相同。

选项式

  1. beforeCreate:实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
  2. created:实例创建完成后被调用。此时已完成数据观测 (data observer),属性和方法的运算,watch/event 事件回调。但是尚未挂载,$el 属性目前不可见。
  3. beforeMount:在挂载开始之前被调用,相关的 render 函数首次被调用。
  4. mounted:实例挂载到 DOM 后调用,这时 el 被新创建的 vm.$el 替换,并挂载到实例上。注意,不能保证它在整个组件树完全渲染完成时才调用。
  5. beforeUpdate:在数据更新导致虚拟 DOM 重新渲染和打补丁之前调用。你可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
  6. updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。
  7. beforeUnmount(在 Vue 2 中称为 beforeDestroy):在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。
  8. unmounted(在 Vue 2 中称为 destroyed):卸载组件后调用。调用此钩子时,组件实例的所有指令都被解绑,所有事件监听器都被移除,所有子组件实例也都会被销毁。

简单的来说,vue3的选项式生命周期函数与vue2的生命周期函数的区别仅仅在于最后两个。

组合式

在 Vue 3 的组合式 API 中,你可以使用 onXXX 形式的函数,来注册生命周期钩子。这些函数可以直接在 setup() 函数或 <script setup> 中使用。

  • onBeforeMount
  • onMounted
  • onBeforeUpdate
  • onUpdated
  • onBeforeUnmount
  • onUnmounted

Vue 3 中,setup 函数涵盖了 beforeCreatecreated 钩子的功能,因此不再需要这两个钩子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { onMounted, onUpdated, onUnmounted } from 'vue';
export default {
setup() {
// 这里相当于 beforeCreate 和 created 的合并作用域
console.log('Setup function: Initialization logic here.');

onMounted(() => {
console.log('Component is mounted!');
});

onUpdated(() => {
console.log('Component has been updated!');
});

onUnmounted(() => {
console.log('Component has been unmounted.');
});

return {};
}
};

模板引用

步骤

  • 调用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
2
3
4
5
6
7
<script setup>
const count = 999
const sayHi = ( )=>{
console.log('打招呼')
}
defineExpose({count,sayHi})//暴露给父组件
</script>

而在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>

    当监听 touchstarttouchmovewheel(滚动)等高频事件时,浏览器的默认行为是:等待事件处理函数执行完毕

    再决定是否执行默认行为(如滚动页面),如果事件处理函数中存在耗时操作(如复杂计算),会导致 滚动卡顿,因为浏览器必须等待函数执行完毕才能滚动页面(默认行为)。

    passive 修饰符的作用,是通过将事件监听器标记为 被动模式(Passive),本质是向浏览器承诺:
    ​“此事件处理函数不会调用 event.preventDefault()”​,从而允许浏览器 ​立即触发默认行为,无需等待函数执行。

    Vue 3 的 .passive 修饰符对应原生 addEventListener{ passive: true } 配置:

    1
    2
    // Vue 编译后的等效代码
    element.addEventListener('scroll', handler, { passive: true });

    .passive 向浏览器承诺 不会阻止默认行为,而 .prevent 的作用是 主动阻止默认行为,二者语义冲突,所以不能同时使用。