Vue 3 响应式系统
第 1 节 概述
Vue 是一个渐进式的前端框架,所谓渐进式就是“可以被逐步集成”的意思,按照需求可以构建简单的静态页面,也可以构建复杂的终端应用:
- 无需构建步骤,渐进式增强静态的 HTML
- 在任何页面中作为 Web Components 嵌入
- 单页应用 (SPA)
- 全栈 / 服务端渲染 (SSR)
- Jamstack / 静态站点生成 (SSG)
- 开发桌面端、移动端、WebGL,甚至是命令行终端中的界面
1.1 创建项目
使用以下命令创建一个 Vue 项目
|
|
根据脚手架可以一步步生成项目:
|
|
1.2 项目组成
项目创建时会生成 index.html,这便是网页的入口,在 index.html 中会创建一个 id 为 app 的 div,并通过 /src/main.ts 向 div 中添加内容。
|
|
在 main. ts 中会创建如下内容,从 vue.js 中加载 createApp 函数,调用该函数,传入 ./App.vue 文件,并将加载的内容挂载到 id 为 app 的 div 中:
|
|
.vue 文件称为组件,在 Vue 3 项目中有大量这样的组件,Vue 3 使用组合式 API,单独定义每个组件中的模板、事件和样式,并将它们组合起来,形成完整的项目。
在单个 .vue 文件中,一般需要定义有如下三个 html 块,在 template 块中定义该组件的模板,使用 html 语言来书写,在 script 块中定义该组件的事件,可以使用 js 或者 ts 来书写,在 style 块中定义该组件的样式,使用 css 样式来书写:
|
|
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>,这里的写法是一种“语法糖”,完整的写法如下:
|
|
setup 函数通过 export 导出,便可以通过 import App from './App.vue' 获取,Vue 3 框架调用方法便可以获取返回值 count,并在渲染模板时将 {{ count }} 替换为 count 变量的值。
通过语法糖,上面的代码可以简写为:
|
|
2.2 ref 函数
上面的代码中用到了 ref 函数,ref 函数对其包裹的变量进行包装,函数会创建一个名为 RefImpl 的对象,对象的 value 就是包裹的变量。

Vue 3 通过 RefImpl 的 getter 和 setter 函数,实现数据的双向绑定,监听到数据变化时,重新渲染页面的 DOM 节点;DOM 节点变化时,调用 setter 函数修改 RefImpl 的值。
|
|
在 templtae 中使用 RefImpl 对象时,不需要输入 .value,Vue 3 会对 RefImpl 对象自动解包,而在 script 中,获取 RefImpl 的值需要 .value,不会自动解包。
2.3 reactive 函数
对对象类型数据进行双向绑定时,除了可以使用 ref 函数外还可以使用 reactive 函数:
|
|
reactive 函数会创建 Proxy 对象,通过 Proxy 对象实现数据的双向绑定。在 script 中获取 Proxy 的值不需要 .value。

ref 函数也可以用于对象类型数据,这种情况下 RefImpl 的 value 就是 Proxy 对象,使用 ref 函数创建对象类型数据的包装类时,在 script 中获取 RefImpl 的值仍然是需要 .value 的。

2.4 toRefs 与 toRef 函数
有时候我们需要对 RefImpl 或者 Proxy 进行解包,取出其中的每一个元素,并进行修改,如果我们这样写:
|
|
就会发现基本类型数据解包后失去了与原 Proxy 的关联,而对象类型数据还保留了关联。

toRefs 函数就是用来解决这一问题的:
|
|
可以看到,不管是基本类型数据还是对象类型数据,都被转成了 ObjectRefImpl 对象,和原 Proxy 保留了关联。

如果 person 是 RefImpl 类型,需要这样写:
|
|
返回值同样是 ObjectRefImpl 对象。
这里需要注意的是,如果使用 ref 函数包裹深层对象,那么使用 toRefs 进行解包后每一层的对象依然是 RefImpl 类型,在语法上具有一致性,对象不会中途变成 Proxy 类型。那么如果总是使用 ref 函数,那么 Proxy 类型就不会出现。所以不使用 reactive 函数,而是全部用 ref 来替代,就是一种常见的偷懒手段。
toRef 函数使用方式类似,可以解包某一特定的元素:
|
|
2.5 计算属性
如果一个属性不是从页面或者后端获取到,而是通过其他属性的值计算出来的,就可以使用计算属性来定义一个全新的属性:
|
|
computed 有 getter 和 setter 方法,上面代码中直接用箭头函数定义计算方法,属于是重写了 getter 方法,computed 函数相比于普通函数的优势在于,其能监听它自身 getter 方法内部其他的 getter 方法的调用(比如 firstName. value),当 firstName 和 lastName 没有变化时,即便调用多次 {{ fullName }},computed 的 getter 方法只会返回上一次的结果,而不会重新计算。也就是,上面的 console.log(1) 只会输出一次。
getter 方法中可以获取上一次的值,方法是在箭头函数中增加对应的参数名:
|
|
computed 还可以设置 setter 方法,当 setter 方法调用时,可以修改其他属性的值。
|
|
2.6 属性绑定
template 中 DOM 节点的属性可以绑定 script 中的变量
ID 绑定
使用 v-bind:id 来进行 id 的绑定,这样 div 的 id 就设置成了 container:
|
|
因为 v-bind 非常常用,Vue 3 对 v-bind 提供了简写语法:
|
|
这里非常特殊,属性名称和 script 中的变量名相同,Vue 3 提供了进一步的简化语法:
|
|
class 绑定
:class 和 class 可以同时存在,Vue 3 会自动将它们合并,大多数的布局、外观的定义可以放到 class 中,少数状态可以放到 :class 中。语法上 :class 可以传入的数据非常灵活,可以是字符串、对象、数组,也可以是它们的组合:
|
|
style 绑定
:style 和 style 也可以同时存在,也会自动合并,语法上,:style 可以传入的数据同样非常灵活:
|
|
其他的 HTML 标签的属性,都可以通过前面加 : 的方式转变为 Vue 3 中可绑定的属性,两者的区别在于,不加 : 的属性是 HTML 标签的属性,会当成普通字符串进行解析,而加了 : 的属性会作为变量或者表达式进行解析,Vue 3 会获取其值并渲染到 DOM 元素中。
绑定多个属性
使用 v-bind 可以一次绑定多个属性:
|
|
2.7 条件渲染
v-if、v-else-if、v-else 指令用于条件渲染,只有当对应的表达式为真时,对应的 DOM 节点才会渲染,否则不会出现在页面中。
|
|
v-show 功能类似,区别在于 v-if、v-else-if、v-else 在条件为 false 的时候不会渲染,只有当条件为 true 时才会渲染,而 v-show 无论条件是否为 true 都会渲染,但是条件为 false 时,display: none。
2.8 列表渲染
对于可遍历的对象,可以使用 v-for 方法获取其每一个元素:
|
|
2.9 事件处理
可以使用 v-on 指令来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。
|
|
v-on 指令可以简写为 @:
|
|
事件修饰符
一般 DOM 事件会经历 3 个阶段:
- 捕获阶段:
window / document -> ... -> target.parent - 目标阶段:
event.target - 冒泡阶段:
parent -> ... -> document / window
为了简化处理 DOM 事件的细节,Vue 3 提供了事件修饰符:
| 修饰符 | 作用 | 常见用法 |
|---|---|---|
.stop |
阻止冒泡 | 内部按钮,不想触发父级点击 |
.prevent |
阻止默认行为 | 阻止表单刷新,阻止 <a> 跳转 |
.self |
只响应自身 | 避免子元素误触 |
.capture |
捕获阶段 | 全局拦截 |
.once |
只触发一次 | 一次性操作,防止重复提交 |
.passive |
性能优化 | 不会调用 preventDefault(),不能和 .prevent 一起用 |
事件修饰符的使用方法如下:

|
|
按键修饰符
按键修饰符包括鼠标按键和键盘按键:
- 鼠标按键修饰符有
.left、.right和middle - 键盘按键修饰符比较多,比如有修饰键
.ctrl、.alt、.shift和.meta,以及一些没有实体字符的按键.esc、.tab、.space、delete(捕获Delete和Backspace两个按键)、.up、.down、.left、.right
比如可以有如下用法:
|
|
如果需要精确控制触发事件的修饰键,可以使用 .exact:
|
|
2.10 表单绑定
要实现表单的双向绑定,原本需要进行绑定值并设定监听事件:
|
|
在 Vue 3 中可以使用 v-model 来简化这一步骤:
|
|
比如说,单行文本框可以如下:
|
|
其他类型的表单基本相似,在通常的 HTML 表单基础上增加 v-model 属性,即可实现数据的双向绑定。
表单有几个特殊的修饰符:
.number 可以将用户输入自动转换为数字
|
|
.trim 可以自动去除用户输入内容中两端的空格
|
|
2.11 侦听器
侦听器可以监测某一个 script 变量的变化情况,并且当变量值发生改变时,调用回调函数,在下面的例子中,watch 接收 3 个参数,第一个是需要侦听的变量或者变量的 getter 函数,第二个是回调函数,第三个是可选参数。
|
|
如果需要侦听的变量是 RefImpl 类型的对象类型数据(使用 ref 函数创建),那么其 value 为 Proxy ,当数据变化时,Proxy 不会变化(除非整个替换 person.value = { name: '李四', age: 20 }),此时就需要使用深层侦听器,方法是在可选参数中增加 { deep: true }:
|
|
对象类型数据为 Proxy 时(使用 reactive 函数创建),无需使用深层侦听器。
通常 watch 默认是懒执行的,仅当数据源变化时,才会执行回调。如果我们需要在创建侦听器时,立即执行一遍回调,需要在可选参数中增加 { immediate: true }。
如果希望回调只在源变化时触发一次,可以在可选参数中增加 { once: true }。
如果我们想要侦听对象类型数据中的某一个特定的值,可以使用如下方式:
|
|
原理是传入一个 getter 函数,这样做有两个原因:
- 一是如果
name是基本类型数据,watch需要侦听其数值的变化(需要传入的参数有getter函数),上述方式可以构建出getter函数。 - 二是,如果
name是对象类型数据,虽然RefImpl和Proxy类型本身有getter函数,但是可能被整个替换,导致person.value.name的地址指向新的RefImpl或者Proxy对象,如果直接传入name,则可能依旧侦听原来的地址。
此外,Vue 3 提供了函数 watchEffect,可以简化 watch 的使用。watchEffect 的作用是立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数。比如下面这个函数,watchEffect 可以自动追踪 name 和 age 的变化。
|
|