第 1 节 概述
Vue 是一个渐进式的前端框架,所谓渐进式就是“可以被逐步集成”的意思,按照需求可以构建简单的静态页面,也可以构建复杂的终端应用:
无需构建步骤,渐进式增强静态的 HTML 在任何页面中作为 Web Components 嵌入 单页应用 (SPA) 全栈 / 服务端渲染 (SSR) Jamstack / 静态站点生成 (SSG) 开发桌面端、移动端、WebGL,甚至是命令行终端中的界面 1.1 创建项目
使用以下命令创建一个 Vue 项目
根据脚手架可以一步步生成项目:
1
2
3
4
5
6
7
8
9
10
11
# 请输入项目名称:
project-name
# 请选择要包含的功能:
TypeScript
JSX 支持
Router ( 单页面应用开发)
Pinia ( 状态管理)
Vitest ( 单元测试)
端到端测试
ESLint ( 错误预防)
Prettier ( 代码格式化)
1.2 项目组成
项目创建时会生成 index.html,这便是网页的入口,在 index.html 中会创建一个 id 为 app 的 div,并通过 /src/main.ts 向 div 中添加内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
< html lang = "" >
< head >
< meta charset = "UTF-8" >
< link rel = "icon" href = "/favicon.ico" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Vite App</ title >
</ head >
< body >
< div id = "app" ></ div >
< script type = "module" src = "/src/main.ts" ></ script >
</ body >
</ html >
在 main. ts 中会创建如下内容,从 vue.js 中加载 createApp 函数,调用该函数,传入 ./App.vue 文件,并将加载的内容挂载到 id 为 app 的 div 中:
1
2
3
4
5
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp ( App )
app . mount ( '#app' )
.vue 文件称为组件,在 Vue 3 项目中有大量这样的组件,Vue 3 使用组合式 API,单独定义每个组件中的模板、事件和样式,并将它们组合起来,形成完整的项目。
在单个 .vue 文件中,一般需要定义有如下三个 html 块,在 template 块中定义该组件的模板,使用 html 语言来书写,在 script 块中定义该组件的事件,可以使用 js 或者 ts 来书写,在 style 块中定义该组件的样式,使用 css 样式来书写:
1
2
3
4
5
< template ></ template >
< script setup lang = "ts" ></ script >
< style scoped ></ style >
1.3 生命周期
Vue 3 的组件从创建到渲染到移除有如下的生命周期,经历 setup、create、mount、update 和 unmount 几个步骤:
setup 是最开始的步骤,对应 <script setup lang="ts"></script> 中的 setup,在这里会初始化组合式 API。create 对应 const app = createApp(App),在这里会初始化选项式 API,选项式 API 多在 Vue 2 项目中使用,通过兼容选项式 API,Vue 3 项目实现了对 Vue 2 项目的支持。mount 对应 app.mount('#app'),实现了页面的渲染,也就是创建和插入 DOM 节点的过程。update 在数据变化时触发,重新渲染页面。在 setup 和 create 步骤中初始化的 API,对数据进行包装并加入钩子函数,实现数据变化的监听。unmount 用于在取消挂载后移除页面的渲染。第 2 节 响应式系统
2.1 setup 函数
在 Vue 3 中,最重要的就是其相应式系统,它实现了数据的双向绑定,在上面的 .vue 文件中,定义了 <script setup lang="ts"></script>,这里的写法是一种“语法糖”,完整的写法如下:
1
2
3
4
5
6
7
8
9
10
11
< template >
{{ count }}
</ template >
< script lang = "ts" >
export default {
setup () {
const count = 0
return { count }
},
}
</ script >
setup 函数通过 export 导出,便可以通过 import App from './App.vue' 获取,Vue 3 框架调用方法便可以获取返回值 count,并在渲染模板时将 {{ count }} 替换为 count 变量的值。
通过语法糖,上面的代码可以简写为:
1
2
3
4
5
6
7
8
9
< template >
{{ count }}
</ template >
< script setup lang = "ts" >
import { ref } from 'vue'
const count = ref ( 0 )
</ script >
2.2 ref 函数
上面的代码中用到了 ref 函数,ref 函数对其包裹的变量进行包装,函数会创建一个名为 RefImpl 的对象,对象的 value 就是包裹的变量。
Vue 3 通过 RefImpl 的 getter 和 setter 函数,实现数据的双向绑定,监听到数据变化时,重新渲染页面的 DOM 节点;DOM 节点变化时,调用 setter 函数修改 RefImpl 的值。
1
2
3
4
5
6
7
8
9
10
< template >
{{ count }}
</ template >
< script setup lang = "ts" >
import { ref } from 'vue'
const count = ref ( 0 )
console . log ( count . value )
</ script >
在 templtae 中使用 RefImpl 对象时,不需要输入 .value,Vue 3 会对 RefImpl 对象自动解包,而在 script 中,获取 RefImpl 的值需要 .value,不会自动解包。
2.3 reactive 函数
对对象类型数据进行双向绑定时,除了可以使用 ref 函数外还可以使用 reactive 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
< template >
< div > 姓名 : {{ person . name }}</ div >
< div > 智力 : {{ person . ability . intelligence }}</ div >
< div > 武器 : {{ person . equipments [ 0 ] }}</ div >
</ template >
< script setup lang = "ts" >
import { reactive } from 'vue'
const person = reactive ({
name : '张三' ,
ability : {
strength : 80 ,
intelligence : 90 ,
},
equipments : [ '拳套' , '轻甲' , '帽子' ],
})
console . log ( person . name )
console . log ( person . ability . strength )
console . log ( person . equipments [ 0 ])
</ script >
reactive 函数会创建 Proxy 对象,通过 Proxy 对象实现数据的双向绑定。在 script 中获取 Proxy 的值不需要 .value。
ref 函数也可以用于对象类型数据,这种情况下 RefImpl 的 value 就是 Proxy 对象,使用 ref 函数创建对象类型数据的包装类时,在 script 中获取 RefImpl 的值仍然是需要 .value 的。
2.4 toRefs 与 toRef 函数
有时候我们需要对 RefImpl 或者 Proxy 进行解包,取出其中的每一个元素,并进行修改,如果我们这样写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const person = reactive ({
name : '张三' ,
ability : {
strength : 80 ,
intelligence : 90 ,
},
equipments : [ '拳套' , '轻甲' , '帽子' ],
})
let { name , ability } = person
console . log ( name )
console . log ( ability )
name = '李四'
ability . intelligence = 70
console . log ( person . name )
console . log ( person . ability . intelligence )
就会发现基本类型数据解包后失去了与原 Proxy 的关联,而对象类型数据还保留了关联。
toRefs 函数就是用来解决这一问题的:
1
2
3
const { name , ability } = toRefs ( person )
console . log ( name )
console . log ( ability )
可以看到,不管是基本类型数据还是对象类型数据,都被转成了 ObjectRefImpl 对象,和原 Proxy 保留了关联。
如果 person 是 RefImpl 类型,需要这样写:
1
2
3
const { name , ability } = toRefs ( person . value )
console . log ( name )
console . log ( ability )
返回值同样是 ObjectRefImpl 对象。
信息
这里需要注意的是,如果使用 ref 函数包裹深层对象,那么使用 toRefs 进行解包后每一层的对象依然是 RefImpl 类型,在语法上具有一致性,对象不会中途变成 Proxy 类型。那么如果总是使用 ref 函数,那么 Proxy 类型就不会出现。所以不使用 reactive 函数,而是全部用 ref 来替代,就是一种常见的偷懒手段。
toRef 函数使用方式类似,可以解包某一特定的元素:
1
2
const name = toRef ( person . value , 'name' )
console . log ( name )
2.5 计算属性
如果一个属性不是从页面或者后端获取到,而是通过其他属性的值计算出来的,就可以使用计算属性来定义一个全新的属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
< template >
{{ fullName }}< br />
{{ fullName }}< br />
{{ fullName }}< br />
</ template >
< script setup lang = "ts" >
import { ref , computed } from 'vue'
const firstName = ref ( 'zhang' )
const lastName = ref ( 'san' )
const fullName = computed (() => {
console . log ( 1 )
return ` ${ firstName . value } ${ lastName . value } `
})
</ script >
computed 有 getter 和 setter 方法,上面代码中直接用箭头函数定义计算方法,属于是重写了 getter 方法,computed 函数相比于普通函数的优势在于,其能监听它自身 getter 方法内部其他的 getter 方法的调用(比如 firstName. value),当 firstName 和 lastName 没有变化时,即便调用多次 {{ fullName }},computed 的 getter 方法只会返回上一次的结果,而不会重新计算。也就是,上面的 console.log(1) 只会输出一次。
getter 方法中可以获取上一次的值,方法是在箭头函数中增加对应的参数名:
1
const fullName = computed (( previous ) => {})
computed 还可以设置 setter 方法,当 setter 方法调用时,可以修改其他属性的值。
1
2
3
4
5
6
7
8
9
10
const fullName = computed ({
get () {
return ` ${ firstName . value } ${ lastName . value } `
},
set ( newValue : string ) {
const [ first = '' , last = '' ] = newValue . split ( ' ' )
firstName . value = first
lastName . value = last
},
})
2.6 属性绑定
template 中 DOM 节点的属性可以绑定 script 中的变量
ID 绑定
使用 v-bind:id 来进行 id 的绑定,这样 div 的 id 就设置成了 container:
1
2
3
4
5
6
7
8
9
< template >
< div v -bind:id = "id" ></ div >
</ template >
< script setup lang = "ts" >
import { ref } from 'vue'
const id = ref ( 'container' )
</ script >
因为 v-bind 非常常用,Vue 3 对 v-bind 提供了简写语法:
1
2
3
< template >
< div :id = "id" ></ div >
</ template >
这里非常特殊,属性名称和 script 中的变量名相同,Vue 3 提供了进一步的简化语法:
1
2
3
< template >
< div : id ></ div >
</ template >
class 绑定
:class 和 class 可以同时存在,Vue 3 会自动将它们合并,大多数的布局、外观的定义可以放到 class 中,少数状态可以放到 :class 中。语法上 :class 可以传入的数据非常灵活,可以是字符串、对象、数组,也可以是它们的组合:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
< template >
< div :class = "div_class" class = "text-white bg-red-500" ></ div >
</ template >
< script setup lang = "ts" >
import { ref } from 'vue'
const div_class = ref ([
{
active : true ,
},
'opacity-50' ,
[ 'px-2' , 'py-2' ],
])
</ script >
style 绑定
:style 和 style 也可以同时存在,也会自动合并,语法上,:style 可以传入的数据同样非常灵活:
1
2
3
4
5
6
7
8
9
10
11
12
13
< template >
< div :style = "style" ></ div >
</ template >
< script setup lang = "ts" >
import { ref } from 'vue'
const style = ref ([
{
color : 'red' ,
fontSize : '30px' ,
},
])
</ script >
信息
其他的 HTML 标签的属性,都可以通过前面加 : 的方式转变为 Vue 3 中可绑定的属性,两者的区别在于,不加 : 的属性是 HTML 标签的属性,会当成普通字符串进行解析,而加了 : 的属性会作为变量或者表达式进行解析,Vue 3 会获取其值并渲染到 DOM 元素中。
绑定多个属性
使用 v-bind 可以一次绑定多个属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
< template >
< div v-bind = "attr" ></ div >
</ template >
< script setup lang = "ts" >
import { ref } from 'vue'
const attr = ref ({
id : 'container' ,
class : 'wrapper' ,
style : 'background-color:green' ,
})
</ script >
2.7 条件渲染
v-if、v-else-if、v-else 指令用于条件渲染,只有当对应的表达式为真时,对应的 DOM 节点才会渲染,否则不会出现在页面中。
1
2
3
4
5
6
7
8
9
10
11
12
13
< template >
< div v-if = "flag1" > 1 </ div >
< div v-else-if = "flag2" > 2 </ div >
< div v-else > 3 </ div >
< div v-show = "flag1" > 2 </ div >
</ template >
< script setup lang = "ts" >
import { ref } from 'vue'
const flag1 = ref ( false )
const flag2 = ref ( true )
</ script >
v-show 功能类似,区别在于 v-if、v-else-if、v-else 在条件为 false 的时候不会渲染,只有当条件为 true 时才会渲染,而 v-show 无论条件是否为 true 都会渲染,但是条件为 false 时,display: none。
2.8 列表渲染
对于可遍历的对象,可以使用 v-for 方法获取其每一个元素:
1
2
3
4
5
6
7
8
9
10
11
12
13
< template >
< li v-for = "(value, key) in items" :key="key" >{{ key }} : {{ value }}</ li >
</ template >
< script setup lang = "ts" >
import { ref } from 'vue'
const items = ref ({
姓名 : '张三' ,
年龄 : '18' ,
性别 : '男' ,
})
</ script >
2.9 事件处理
可以使用 v-on 指令来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。
1
2
3
4
5
6
7
8
9
10
11
12
13
< template >
< div > 当前点击次数 : {{ count }}</ div >
< button v -on:click = "handleClick" > 点击 </ button >
</ template >
< script setup lang = "ts" >
import { ref } from 'vue'
const count = ref ( 0 )
const handleClick = () => {
count . value ++
}
</ script >
v-on 指令可以简写为 @:
1
2
3
4
5
6
7
8
9
10
11
12
13
< template >
< div > 当前点击次数 : {{ count }}</ div >
< button @click ="handleClick" > 点击 </ button >
</ template >
< script setup lang = "ts" >
import { ref } from 'vue'
const count = ref ( 0 )
const handleClick = () => {
count . value ++
}
</ script >
事件修饰符
一般 DOM 事件会经历 3 个阶段:
捕获阶段:window / document -> ... -> target.parent 目标阶段:event.target 冒泡阶段:parent -> ... -> document / window 为了简化处理 DOM 事件的细节,Vue 3 提供了事件修饰符:
修饰符 作用 常见用法 .stop阻止冒泡 内部按钮,不想触发父级点击 .prevent阻止默认行为 阻止表单刷新,阻止 <a> 跳转 .self只响应自身 避免子元素误触 .capture捕获阶段 全局拦截 .once只触发一次 一次性操作,防止重复提交 .passive性能优化 不会调用 preventDefault(),不能和 .prevent 一起用
事件修饰符的使用方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
< template >
< div @click ="parentClick" >
< button @click.stop ="childClick" > 点我 </ button >
</ div >
</ template >
< script setup lang = "ts" >
const parentClick = () => {
console . log ( 'parentClick' )
}
const childClick = () => {
console . log ( 'childClick' )
}
</ script >
按键修饰符
按键修饰符包括鼠标按键和键盘按键:
鼠标按键修饰符有 .left 、.right 和 middle 键盘按键修饰符比较多,比如有修饰键 .ctrl、.alt、.shift 和 .meta,以及一些没有实体字符的按键 .esc 、.tab 、.space、delete (捕获 Delete 和 Backspace 两个按键)、.up、.down、.left、.right 比如可以有如下用法:
1
2
3
4
< input @keyup.enter ="submit" />
< textarea @keyup.ctrl.enter ="send" />
< div @click.right.prevent ="openMenu" />
< input @keydown.alt.a.exact ="onAltA" />
如果需要精确控制触发事件的修饰键,可以使用 .exact:
1
2
3
4
5
6
<!-- 当按下Ctrl时 , 即使同时按下Alt或Shift也会触发 -->
< button @click.ctrl ="onClick" > A </ button >
<!- -仅当按下Ctrl且未按任何其他键时才会触发 -- >
< button @click.ctrl.exact ="onCtrlClick" > A </ button >
<!- -仅当没有按下任何系统按键时触发 -- >
< button @click.exact ="onClick" > A </ button >
2.10 表单绑定
要实现表单的双向绑定,原本需要进行绑定值并设定监听事件:
1
2
3
< input
:value = "text"
@input ="event => text = event.target.value" >
在 Vue 3 中可以使用 v-model 来简化这一步骤:
比如说,单行文本框可以如下:
1
2
< p > Message is : {{ message }}</ p >
< input v-model = "message" placeholder="edit me" />
信息
其他类型的表单基本相似,在通常的 HTML 表单基础上增加 v-model 属性,即可实现数据的双向绑定。
表单有几个特殊的修饰符:
.number 可以将用户输入自动转换为数字
1
< input v -model .number =" age " />
.trim 可以自动去除用户输入内容中两端的空格
1
< input v -model .trim =" msg " />
2.11 侦听器
侦听器可以监测某一个 script 变量的变化情况,并且当变量值发生改变时,调用回调函数,在下面的例子中,watch 接收 3 个参数,第一个是需要侦听的变量或者变量的 getter 函数,第二个是回调函数,第三个是可选参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
< template >
< div > 当前点击次数 : {{ count }}</ div >
< button v -on:click = "handleClick" > 点击 </ button >
</ template >
< script setup lang = "ts" >
import { ref , watch } from 'vue'
const count = ref ( 0 )
const handleClick = () => {
count . value ++
}
watch (
count ,
( newValue , oldValue ) => {
console . log ( 'count变化了' , newValue , oldValue )
},
{ immediate : true },
)
</ script >
如果需要侦听的变量是 RefImpl 类型的对象类型数据(使用 ref 函数创建),那么其 value 为 Proxy ,当数据变化时,Proxy 不会变化(除非整个替换 person.value = { name: '李四', age: 20 }),此时就需要使用深层侦听器,方法是在可选参数中增加 { deep: true }:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const person = ref ({
name : '张三' ,
age : 18 ,
})
const handleClick = () => {
person . value . name = '李四'
person . value . age = 20
}
watch (
person ,
( value ) => {
console . log ( 'person变化了' , value )
},
{ deep : true },
)
对象类型数据为 Proxy 时(使用 reactive 函数创建),无需使用深层侦听器。
通常 watch 默认是懒执行的,仅当数据源变化时,才会执行回调。如果我们需要在创建侦听器时,立即执行一遍回调,需要在可选参数中增加 { immediate: true }。
如果希望回调只在源变化时触发一次,可以在可选参数中增加 { once: true }。
如果我们想要侦听对象类型数据中的某一个特定的值,可以使用如下方式:
1
2
3
4
5
6
watch (
() => person . value . name ,
( value ) => {
console . log ( 'name变化了' , value )
},
)
原理是传入一个 getter 函数,这样做有两个原因:
一是如果 name 是基本类型数据,watch 需要侦听其数值的变化(需要传入的参数有 getter 函数),上述方式可以构建出 getter 函数。 二是,如果 name 是对象类型数据,虽然 RefImpl 和 Proxy 类型本身有 getter 函数,但是可能被整个替换,导致 person.value.name 的地址指向新的 RefImpl 或者 Proxy 对象,如果直接传入 name,则可能依旧侦听原来的地址。 此外,Vue 3 提供了函数 watchEffect,可以简化 watch 的使用。watchEffect 的作用是立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数。比如下面这个函数,watchEffect 可以自动追踪 name 和 age 的变化。
1
2
3
watchEffect (() => {
person . value . name = person . value . age + '岁的' + person . value . name
})