响应式
ref() 接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
在模板中使用 ref 时,我们不需要附加 .value。为了方便起见,ref 会自动解包。
注意,在模板渲染上下文中,只有顶级的 ref 属性才会被解包。
<script setup> 中的顶层的导入、声明的变量和函数可在同一组件的模板中直接使用。你可以理解为模板是在同一作用域内声明的一个 JavaScript 函数——它自然可以访问与它一起声明的所有内容。
Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map。Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到。
非原始值将通过 reactive()
转换为响应式代理。
计算属性
computed() 方法期望接收一个 getter 函数,返回值为一个计算属性 ref。
Vue 的计算属性会自动追踪响应式依赖。
计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。
计算属性默认是只读的。
最佳实践:
Getter 不应有副作用。
计算属性的 getter 应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记。举例来说,不要改变其他状态、在 getter 中做异步请求或者更改 DOM!一个计算属性的声明中描述的是如何根据其他值派生一个值。因此 getter 的职责应该仅为计算和返回该值。避免直接修改计算属性值
从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。
侦听器
计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。
在组合式 API 中,我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数。
watchEffect() 允许我们自动跟踪回调的响应式依赖。
对于有多个依赖项的侦听器来说,使用 watchEffect() 可以消除手动维护依赖列表的负担。
如果你需要侦听一个嵌套数据结构中的几个属性,watchEffect() 可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。
表单
v-model 会根据所使用的元素自动使用对应的 DOM 属性和事件组合:
- 文本类型的 <input> 和 <textarea> 元素会绑定 value property 并侦听 input 事件;
- <input type="checkbox"> 和 <input type="radio"> 会绑定 checked property 并侦听 change 事件;
- <select> 会绑定 value property 并侦听 change 事件。
组合式函数
“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。
你还可以嵌套多个组合式函数:一个组合式函数可以调用一个或多个其他的组合式函数。这使得我们可以像使用多个组件组合成整个应用一样,用多个较小且逻辑独立的单元来组合形成复杂的逻辑。实际上,这正是为什么我们决定将实现了这一设计模式的 API 集合命名为组合式 API。
模板引用
ref 是一个特殊的 attribute,允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。
<script setup>
import { ref, onMounted } from 'vue'
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="input" />
</template>
当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素。
const itemRefs = ref([])
除了使用字符串值作名字,ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">
注意我们这里需要使用动态的 :ref 绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null。你当然也可以绑定一个组件方法而不是内联函数。
模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例。
使用了 <script setup> 的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露。
状态管理
Store (如 Pinia) 是一个保存状态和业务逻辑的实体,它并不与你的组件树绑定。换句话说,它承载着全局状态。它有点像一个永远存在的组件,每个组件都可以读取和写入它。它有三个概念,state、getter 和 action,我们可以假设这些概念相当于组件中的 data
、 computed
和 methods
。
路由
服务端路由指的是服务器根据用户访问的 URL 路径返回不同的响应结果。当我们在一个传统的服务端渲染的 web 应用中点击一个链接时,浏览器会从服务端获得全新的 HTML,然后重新加载整个页面。
然而,在单页面应用中,客户端的 JavaScript 可以拦截页面的跳转请求,动态获取新的数据,然后在无需重新加载的情况下更新当前页面。这样通常可以带来更顺滑的用户体验,尤其是在更偏向“应用”的场景下,因为这类场景下用户通常会在很长的一段时间中做出多次交互。
在这类单页应用中,“路由”是在客户端执行的。一个客户端路由器的职责就是利用诸如 History API 或是 hashchange
事件这样的浏览器 API 来管理应用当前应该渲染的视图。
通过 Vue.js,我们已经用组件组成了我们的应用。当加入 Vue Router 时,我们需要做的就是将我们的组件映射到路由上,让 Vue Router 知道在哪里渲染它们。
声明式描述 UI
Vue.js 是一个声明式的框架。声明式的好处在于,它直接描述结果,用户不需要关注过程。Vue.js 采用模板的方式来描述 UI,但它同样支持使用虚拟 DOM 来描述 UI。虚拟 DOM 要比模板更加灵活,但模板要比虚拟 DOM 更加直观。
渲染机制
虚拟 DOM (Virtual DOM,简称 VDOM) 是一种编程概念,意为将目标所需的 UI 通过数据结构“虚拟”地表示出来,保存在内存中,然后将真实的 DOM 与之保持同步。
一个运行时渲染器将会遍历整个虚拟 DOM 树,并据此构建真实的 DOM 树。这个过程被称为挂载 (mount)。
如果我们有两份虚拟 DOM 树,渲染器将会有比较地遍历它们,找出它们之间的区别,并应用这其中的变化到真实的 DOM 上。这个过程被称为更新 (patch),又被称为“比对”(diffing) 或“协调”(reconciliation)。
虚拟 DOM 带来的主要收益是它让开发者能够灵活、声明式地创建、检查和组合所需 UI 的结构,同时只需把具体的 DOM 操作留给渲染器去处理。
从高层面的视角看,Vue 组件挂载时会发生如下几件事:
- 编译:Vue 模板被编译为渲染函数:即用来返回虚拟 DOM 树的函数。这一步骤可以通过构建步骤提前完成,也可以通过使用运行时编译器即时完成。
- 挂载:运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树,并基于它创建实际的 DOM 节点。这一步会作为响应式副作用执行,因此它会追踪其中所用到的所有响应式依赖。
-
更新:当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。
Vue 模板会被预编译成虚拟 DOM 渲染函数。
模板 vs. 渲染函数
Vue 模板会被预编译成虚拟 DOM 渲染函数。Vue 也提供了 API 使我们可以不使用模板编译,直接手写渲染函数。在处理高度动态的逻辑时,渲染函数相比于模板更加灵活,因为你可以完全地使用 JavaScript 来构造你想要的 vnode。
那么为什么 Vue 默认推荐使用模板呢?有以下几点原因:
- 模板更贴近实际的 HTML。这使得我们能够更方便地重用一些已有的 HTML 代码片段,能够带来更好的可访问性体验、能更方便地使用 CSS 应用样式,并且更容易使设计师理解和修改。
- 由于其确定的语法,更容易对模板做静态分析。这使得 Vue 的模板编译器能够应用许多编译时优化来提升虚拟 DOM 的性能表现 (下面我们将展开讨论)。
在实践中,模板对大多数的应用场景都是够用且高效的。渲染函数一般只会在需要处理高度动态渲染逻辑的可重用组件中使用。
渲染器的作用是,把虚拟 DOM 对象渲染为真实 DOM 元素。它的工作原理是,递归地遍历虚拟 DOM 对象,并调用原生 DOM API 来完成真实 DOM 的创建。
组件其实就是一组虚拟 DOM 元素的封装。
响应式系统
Vue 最标志性的功能就是其低侵入性的响应式系统。组件状态都是由响应式的 JavaScript 对象组成的。当更改它们时,视图会随即自动更新。这让状态管理更加简单直观。
本质上,响应性是一种可以使我们声明式地处理变化的编程范式。
在 JavaScript 中有两种劫持 property 访问的方式:getter / setters 和 Proxies。Vue 2 使用 getter / setters 完全是出于支持旧版本浏览器的限制。而在 Vue 3 中则使用了 Proxy 来创建响应式对象,仅将 getter / setter 用于 ref。
Vue 的响应式系统基本是基于运行时的。追踪和触发都是在浏览器中运行时进行的。运行时响应性的优点是,它可以在没有构建步骤的情况下工作,而且边界情况较少。另一方面,这使得它受到了 JavaScript 语法的制约,导致需要使用一些例如 Vue ref 这样的值的容器。
单文件组件
Vue 的单文件组件 (即 *.vue 文件,英文 Single-File Component,简称 SFC) 是一种特殊的文件格式,使我们能够将一个 Vue 组件的模板、逻辑与样式封装在单个文件中。
在现代的 UI 开发中,我们发现与其将代码库划分为三个巨大的层,相互交织在一起,不如将它们划分为松散耦合的组件,再按需组合起来。在一个组件中,其模板、逻辑和样式本就是有内在联系的、是耦合的,将它们放在一起,实际上使组件更有内聚性和可维护性。
依赖注入
一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
provide() 函数接收两个参数。第一个参数被称为注入名;第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref:
import { ref, provide } from 'vue'
const count = ref(0)
provide('key', count)
提供的响应式状态使后代组件可以由此和提供者建立响应式的联系。
当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。
有的时候,我们可能需要在注入方组件中更改数据。在这种情况下,我们推荐在供给方组件内声明并提供一个更改数据的方法函数:
<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'
const location = ref('North Pole')
function updateLocation() {
location.value = 'South Pole'
}
provide('location', {
location,
updateLocation
})
</script>
<!-- 在注入方组件 -->
<script setup>
import { inject } from 'vue'
const { location, updateLocation } = inject('location')
</script>
<template>
<button @click="updateLocation">{{ location }}</button>
</template>
getter 函数
有时需要允许访问返回动态计算值的属性,或者你可能需要反映内部变量的状态,而不需要使用显式方法调用。在 JavaScript 中,可以使用 getter 来实现。
语法:
{ get prop() { ... } }
{ get [expression]() { ... } }
示例:
const obj = {
get hello() {
return 'world'
}
}
console.log(obj.hello) // world
使用计算出的属性名
var expr = "foo";
var obj = {
get [expr]() {
return "bar";
},
};
console.log(obj.foo); // "bar"
使用defineProperty
在现有对象上定义 getter
var o = { a: 0 };
Object.defineProperty(o, "b", {
get: function () {
return this.a + 1;
},
});
console.log(o.b); // Runs the getter, which yields a + 1 (which is 1)