Vue 简介
来自官网 https://cn.vuejs.org
1. 概况
两个核心功能:
声明式渲染:Vue 基于标准 HTML 拓展了一套语法
响应式:JavaScript 状态改变时会(自动)响应式地更新 DOM
编程模型:声明式、组件化(单文件组件)
1.1 使用 Vue 的方式
构建 Web 应用
引入 JS 文件:无需构建步骤,渐进式增强静态的 HTML
作为 Web Components 嵌入
单页面应用(Single-Page Application,SPA)
特征:前端有丰富的交互性、较深的会话和复杂的状态逻辑
问题:纯客户端的 SPA 在首屏加载和 SEO 方面,因为浏览器会收到一个巨大的 HTML 空页面,只有等到 JavaScript 加载完毕才会渲染出内容
服务端渲染(Server-Side Rendering,SSR)
好处:能极大地改善应用在 Web 核心指标上的性能表现
静态站点生成:所需数据是静态的,所以服务端渲染可以提前完成,即将站点预渲染为静态 HTML
Web 之外
桌面应用
移动端应用
渲染器,比如 WebGL、终端命令行
更多内容可查阅使用 Vue 的多种方式,Vue 的核心知识在这些不同的使用方式中都是通用的。
1.2 组件的书写风格
Options API,选项式 API
用一个对象来描述组件的逻辑
适用场景:不需要使用构建工具,或者打算主要在低复杂度的场景中使用 Vue
Composition API,组合式 API
可以使用导入的 API 函数来描述组件逻辑
适用场景:打算用 Vue 构建完整的单页应用,推荐采用组合式 API + 单文件组件
实际上,选项式 API 是在组合式 API 的基础上实现的。大部分的核心概念在这两种风格之间都是通用的,熟悉了一种风格以后也能很快地理解另一种风格。
选项式 API 以“组件实例”的概念为中心 (即上述例子中的 this),对于有面向对象语言背景的用户来说,这通常与基于类的心智模型更为一致。同时,它将响应性相关的细节抽象出来,并强制按照选项来组织代码,从而对初学者而言更为友好。
组合式 API 的核心思想是直接在函数作用域内定义响应式状态变量,并将从多个函数中得到的状态组合起来处理复杂问题。这种形式更加自由,也需要你对 Vue 的响应式系统有更深的理解才能高效使用。相应的,它的灵活性也使得组织和重用逻辑的模式变得更加强大。
1.3 组合式 API
组合式 API 是 Vue 3 和 Vue 2.7 的内置功能。在 Vue 3 中,组合式 API 基本上都会配合 <script setup>
语法在单文件组件中使用。
组合式 API 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它是一个概括性的术语,涵盖了以下方面的 API:
说明:虽然这套 API 的风格是基于函数的组合,但组合式 API 并不是函数式编程。组合式 API 是以 Vue 中数据可变的、细粒度的响应性系统为基础的,而函数式编程通常强调数据不可变。
组合式 API 的好处:
更好的逻辑复用:
使我们能用组合式函数。选项式 API 主要的逻辑复用机制是 mixins,而组合式 API 解决了 mixins 的所有缺陷
更灵活的代码组织
当需要处理多个逻辑关注点的组件时,选项式 API 会有不便之处(处理相同逻辑关注点的代码被强制拆分在了不同的选项中)
也方便重构,利于后期维护
更好的类型推导:组合式 API 主要利用基本的变量和函数,它们本身就是类型友好的
选项式 API(2013 年)的类型推导很复杂,且在处理 mixins 和依赖注入类型时依然不太理想
想用 TS 的 Vue 开发者用了由
vue-class-component
提供的 Class API(非常依赖 ES 装饰器,2019 年时还是个不稳定的语言特性)基于 Class 的 API 和选项式 API 在逻辑复用和代码组织方面存在相同的限制
更小的生产包体积
<script setup>
书写的组件模板被编译为了一个内联函数,和<script setup>
的代码位于同一个作用域,所以本地变量的名字可以被压缩选项式 API 需要依赖
this
上下文对象来访问属性,然而对象的属性名不能被压缩
组合式 API 能够覆盖所有状态逻辑方面的需求,如果使用 <script setup>
,那么 inheritAttrs
应该是唯一一个需要用额外的 <script>
块书写的选项了。Vue 的组合式 API 仅调用 setup()
或 <script setup>
的代码一次。
1.4 单文件组件
单文件组件,Single-File Component,简称 SFC,将一个 Vue 组件的模板、逻辑与样式封装在单个文件中,完整的语法定义详见 SFC 语法定义。
Vue SFC 是一个框架指定的文件格式,会由 @vue/compiler-sfc 编译为标准的 JavaScript 和 CSS,一个编译后的 SFC 是一个标准的 JavaScript(ES) 模块。可在 Vue SFC Playground 中看到被最终编译后的样子。
在实际项目中,我们一般会使用集成了 SFC 编译器的构建工具,比如 Vite 或者 Vue CLI (基于 webpack),Vue 官方也提供了脚手架工具来帮助我们尽可能快速地上手开发 SFC。
1.5 本地构建
Vue CLI, Command Line Interface
Vite
Vue 官方的构建流程是基于 Vite 的。需要安装 15.0 或更高版本的 Node.js。关于如何为一个 Vite 项目配置 Vue 相关的特殊行为,可查看 @vitejs/plugin-vue。
使用 Vite 来创建一个 Vue 项目,运行 npm init vue@latest
命令。它会安装和执行 create-vue,一个 Vue 提供的官方脚手架工具:
推荐 IDE 配置:VS Code + Volar 扩展
Volar 提供了语法高亮、TypeScript 支持,以及模板内表达式与组件 props 的智能提示
Volar 取代了之前为 Vue 2 提供的官方 VSCode 扩展 Vetur
浏览器开发插件 Vue.js devtools。可以浏览一个 Vue 应用的组件树,查看各个组件的状态,追踪状态管理的事件,还可以进行组件性能分析。
eslint-plugin-vue,一个 ESLint 插件,会提供 SFC 相关规则的定义。由 Vue 团队维护
底层库
@vue/compiler-sfc
提供了处理 Vue SFC 的底层的功能@vitejs/plugin-vue
为 Vite 提供 Vue SFC 支持的官方插件
为 webpack 提供 Vue SFC 支持的官方 loadervue-loader
更多工具,详见官网文档 工具链。
2. 创建应用
2.1 创建一个新的应用实例
每个 Vue 应用都是通过 createApp()
函数创建一个新的应用实例,比如:
传入 createApp()
的对象实际上是一个组件,我们也可以这样写:
每个应用都需要一个根组件,其它组件将作为其子组件。大多数真实的应用都是由一棵嵌套的、可重用的组件树组成的。
关于应用实例的几点说明:
应用实例会暴露一个
.config
对象允许我们配置一些应用级的选项比如配置应用级的错误处理器
比如显式地添加全局对象,供所有的 Vue 表达式使用
应用实例提供了一些方法来注册应用范围内可用的资源
应用实例并不只限于一个。
createApp()
允许我们在同一个页面中创建多个共存的 Vue 应用,而且每个应用都拥有自己的用于配置和全局资源的作用域,比如在服务端渲染时通常会创建多个小的应用实例,然后将它们分别挂载到所需的元素上去。
2.2 挂载应用
应用实例只有在调用了 .mount()
方法后才会被渲染出来,即挂载应用。mount()
方法接收一个“容器”参数,可以是一个实际的 DOM 元素,也可以是一个 CSS 选择器字符串。
需注意:
.mount()
方法的返回值是根组件实例而不是应用实例(这不同于其它资源注册方法).mount()
方法应该始终在整个应用配置和资源注册完成后被调用确保在挂载应用实例之前,完成所有应用配置!
3. 模板语法
Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。 在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。
当然,也可以不使用模板,而直接使用 JavaScript——采用渲染函数 & JSX 的方式。需要注意的是,此时将不会享受到和模板同等级别的编译时优化。
3.1 数据绑定
文本插值
{{ msg }}
:最基本的数据绑定形式指令
v-bind
响应式地绑定一个 attribute指令
v-html
插入纯 HTML,比如<span v-html="rawHtml"></span>
插值为纯 HTML,将会忽略数据绑定
不建议在网站上动态渲染任意 HTML,因为非常容易造成 XSS 漏洞
在使用 Vue 时,应当使用组件作为 UI 重用和组合的基本单元
在所有的数据绑定中都支持完整的 JavaScript 表达式,作用域是组件。相关说明:
每个绑定仅支持单一表达式,可以简单地理解为可以合法地写在
return
后面的绑定在表达式中的方法在组件每次更新时都会被重新调用,因此不应该产生任何副作用(修改/删除)
模板中的表达式将被沙盒化,仅能够访问到有限的全局对象列表(
app.config
可添加应用级)
3.2 指令
Vue 指令,以 v-
开头的特殊 attribute。
指令 attribute 的期望值为一个 JavaScript 表达式 (除了少数几个例外,v-for
, v-on
, v-slot
)。一个指令的任务是在其表达式的值变化时响应式地更新 DOM。
完整的指令语法:
指令名
指令参数,其中动态参数的值和语法都有相应的限制
指令修饰符
指令值
常用指令的用法,见另一篇笔记《Vue 指令》。
3.3 模板引用
虽然 Vue 的声明性渲染模型已经为我们抽象了大部分对 DOM 的直接操作,但有时候依然需要直接访问底层 DOM 元素,比如将焦点设置在某个 input 元素上,比如要在一个元素上初始化一个第三方库。
这时就用到了一个特殊的 attribute ref
(模板引用),它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。
需要声明一个 ref 来存放该元素的引用,必须和模板里的 ref 同名。比如:
使用时机
我们只可以在组件挂载后才能访问模板引用
如果是想在模板中的表达式上访问模板引用,那需要写个侦听器
watchEffect
,因为初次渲染时模板引用的值会是null
更多场景
v-for
中的模板引用函数模板引用
组件上的 ref
4. 响应式
4.1 创建一个响应式对象或数组
使用 reactive()
函数创建一个响应式对象或数组。
reactive()
返回的是响应式对象其实是 JavaScript Proxy,详见 深入响应式系统。
响应式代理 vs 原始对象
Proxy 对象和原始对象是不相等的
只有代理对象是响应式的,更改原始对象不会触发更新
reactive()
API 的两条限制仅对对象类型有效,对原始类型无效
因为 Vue 的响应式系统是通过属性访问进行追踪的,所以我们必须始终保持对该响应式对象的相同引用
不可以随意地“替换”一个响应式对象,这将导致对初始引用的响应性连接丢失
当我们将响应式对象的属性赋值或解构至本地变量时,或是将该属性传入一个函数时,我们会失去响应性
深层响应式对象 vs 浅层响应式对象
DOM 的更新时机
DOM 的更新并不是同步的,Vue 会缓冲它们直到更新周期的“下个时机”,以确保无论我们进行了多少次声明更改,每个组件都只需要更新一次
全局 API
nextTick()
可确保等待一个状态改变后的 DOM 更新完成
4.2 使用响应式状态
要在组件模板中使用响应式状态,需要在 setup()
函数中定义并返回。比如:
考虑到在 setup()
函数中手动暴露大量的状态和方法会非常繁琐,通常我们会使用 <script setup>
来大幅度地简化代码。如下:
4.3 响应式引用
reactive()
的种种限制归根结底是因为 JavaScript 没有可以作用于所有值类型的“引用”机制。为此,Vue 提供了一个 ref()
方法允许我们创建可以使用任何值类型的响应式 ref。
4.3.1 用 ref()
定义响应式变量
ref()
定义响应式变量ref()
方法将传入的参数的值,包装成一个带 .value
属性的 ref 对象:
和响应式对象的属性类似,ref 的 .value
属性也是响应式的。
4.3.2 响应式 ref 的优势
可作用于所有值:包括原始值和对象
一个包含对象类型值的 ref 可以响应式地替换整个对象
ref 被传递给函数或是从一般对象上被解构时,不会丢失响应性
简而言之,ref()
让我们能创造一种对任意值的“引用”,并能在不丢失响应性的前提下传递这些引用。这个功能很重要,因为它经常用于将逻辑提取到组合式函数中。
4.3.3 响应式 ref 的解包
解包,即不需要写 .value
。
ref 在模板中的解包:仅当 ref 是模板渲染上下文的顶层属性时才适用自动“解包”
ref 在响应式对象中的解包:只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包
数组和集合类型的 ref 解包:跟响应式对象不同,当 ref 作为响应式数组或像 Map 这种原生集合类型的元素被访问时,不会进行解包
Vue 提供了一种编译时转换,可以让编译器帮我们省去使用写
.value
的麻烦,详见响应性语法糖
4.4 计算属性
计算属性可以用来描述依赖响应式状态的复杂逻辑。
computed()
方法期望接收一个 getter 函数,返回值为一个计算属性 ref。
计算属性 ref 会在模板中自动解包,所以在模板表达式中引用时无需添加
.value
计算属性会自动追踪响应式依赖
它会检测到依赖于
author.books
,所以当author.books
改变时,任何依赖于publishedBooksMessage
的绑定都会同时更新
计算属性 vs 方法
一个计算属性仅会在其响应式依赖更新时才重新计算,即值会被缓存
方法调用总是会在重渲染发生时再次执行函数
在某些特殊场景中,我们可以通过同时提供 getter 和 setter 来创建 computed()
:
最佳实践:
计算函数不应有副作用。一个计算属性的声明中描述的是如何根据其他值派生一个值
避免直接修改计算属性值。应该更新它所依赖的源状态以触发新的计算
4.5 有副作用的状态变化
计算属性能让我们声明性地计算衍生值。但有时,需要在响应式状态变化时执行一些副作用,比如更改 DOM,比如根据异步操作的结果去修改另一处的状态。
此时,我们可以使用 watch
和 watchEffect
,它们都能响应式地执行有副作用的回调。
watch
和 watchEffect
的区别如下:
执行时机
watch
是懒执行的。仅当数据源变化时,才会执行回调watchEffect
会在创建侦听器时,立即执行一遍回调。
追踪响应式依赖的方式(主要区别)
watch
只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch
会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。watchEffect
则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
更多内容:
回调的触发时机
Vue 更新之前的 DOM:默认情况,侦听器回调是在 Vue 组件更新之前被调用,也就是说在侦听器回调中访问的 DOM 是被 Vue 更新之前的状态
Vue 更新之后的 DOM:需增加选项
flush: 'post'
,后置刷新有个别名watchPostEffect
回调:同步 vs 异步
watchEffect
仅会在其同步执行期间追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪
深层侦听器(注意性能问题)
必须用同步语句创建侦听器(关键点)
用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止
若是用异步回调创建的侦听器,那么它不会绑定到当前组件上,我们必须手动停止它以防内存泄漏
5. 生命周期
每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,在此期间它也会运行被称为生命周期钩子的函数,以便让开发者有机会在特定阶段运行自己的代码。
实例的生命周期的图表,如下:
渲染遇到了组件
setup
组合式 APIbeforeCreate
设置好数据侦听(初始化选项 API)
created
编译模板
开始渲染(插入到 DOM 树)
beforeMount
mounted
挂载实例到 DOM(mounted)
在数据改变时更新 DOM
beforeUpdate
updated
卸载 unmounted
beforeUnmount
unmounted
最常用的是 onMounted
, onUpdated
和 onUnmounted
。
所有生命周期钩子的完整参考及其用法请参考生命周期钩子 API。
6. 组件
6.1 组件模型
Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。Vue 同样也能很好地配合原生 Web Component,详见 Vue 与 Web Components。
定义组件
使用构建:将 Vue 组件定义在一个单独的
.vue
文件中(单文件组件,简称 SFC)不使用构建:一个 Vue 组件以一个包含 Vue 特定选项的 JavaScript 对象来定义
使用组件
在父组件中导入它 or 全局地注册一个组件。详见全局注册 vs 局部注册
组件可以被重用任意多次。每当使用一个组件,就创建了一个新的实例
组件的标签名:推荐用驼峰 PascalCase,以和原生 HTML 元素作区分
传递 props
props 是一种特别的 attributes,用宏
defineProps
定义组件自己的属性defineProps
是一个仅<script setup>
中可用的编译宏命令,并不需要显式地导入
使用时,就像使用自定义 attribute 一样
<BlogPost :title="xx"/>
监听事件
父组件可以通过
v-on
或@
来选择性地监听子组件上抛的事件,就像监听原生 DOM 事件那样子组件可以通过调用内置的
$emit
方法,通过传入事件名称来抛出一个事件
插槽
<slot>
:作为一个占位符,来显示父组件传进来的内容动态组件:在两个组件间来回切换,
<component :is="...">
直接在 DOM 中编写模板
6.2 内置组件
帮助制作基于状态变化的过渡和动画(更多动画技巧)
<Transition>
<TransitionGroup>
在多个组件间动态切换时缓存被移除的组件实例:
<KeepAlive>
在两个组件间来回切换,比如 Tab 界面
将组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去:
<Teleport>
比如全屏的模态框
在组件树中协调对异步依赖的处理:
<Suspense>
7. 路由
在这单页应用中,“路由”是在客户端执行的,利用诸如 History API 或是 hashchange 事件这样的浏览器 API 来管理应用当前应该渲染的视图。
Vue 提供了官方的路由库 vue-router。
Last updated