简介
组合式api vs 选项式api
- 写法结构不同
选项式 API(Options API)
用各种「选项」组织代码,比如 data、methods、computed、watch,每个功能点有各自固定的位置。
<script>
export default {
data() {
return {
count: 0,
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
组合式 API(Composition API)
把逻辑用函数组合在一起,在 setup() 里统一管理。自己控制变量和函数的定义和组织。
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
</script>
- 响应式实现方式不同
-
选项式:Vue 自动帮你把
data里的数据变成响应式的(底层是用Object.defineProperty或 Proxy)。 -
组合式:要手动用
ref()或reactive()创建响应式数据。
- 逻辑复用方式不同
- 选项式:通常靠
mixins,但是 mixins 很容易命名冲突、来源不明确。
mixins是什么 在 Vue(选项式 API)里,mixin(混入)就是一个可以被多个组件共享的对象。
这个对象里可以写data、methods、computed、watch等,就像正常组件里写的一样。 然后组件通过mixins: []把它们「混进来」,实现逻辑复用。 如果 mixin 和组件都定义了同名的data、methods,容易搞混是谁定义的。
- 组合式:可以直接封装成自定义的 逻辑函数(叫 composables,比如
useXXX()),清晰且可组合。
- 维护难易
-
选项式:一个组件里,不同功能点分散在各个选项块里(比如 data 写一块,methods 写一块),功能之间分开但看起来很分散。
-
组合式:可以把一整块相关逻辑集中在一起,功能是聚合的,代码更模块化。
创建一个vue应用
应用实例
每个 Vue 应用都是通过 createApp 函数创建一个新的 应用实例:
import { createApp } from 'vue'
const app = createApp({
/* 根组件选项 */
})
根组件
我们传入 createApp 的对象实际上是一个组件,每个应用都需要一个“根组件”,其他组件将作为其子组件。
如果你使用的是单文件组件,我们可以直接从另一个文件中导入根组件。
import { createApp } from 'vue'
// 从一个单文件组件中导入根组件
import App from './App.vue'
const app = createApp(App)
根应用 在 Vue 里,你会写很多组件(比如:导航栏组件、商品列表组件、登录表单组件等等)。 根组件就是最顶层的那个组件,它是最早被创建、挂载到页面上的。 其它的组件,都是被根组件引入和嵌套进去的。
为什么需要根组件?
- 你得有个“起点”,告诉 Vue:“从这里开始渲染整个页面!”
createApp(App)就是说:"以App这个组件为入口,开始构建整个界面树"。
大多数真实的应用都是由一棵嵌套的、可重用的组件树组成的。
网页
└── 根组件 App
├── 头部组件 Header
├── 主体组件 Main
│ ├── 文章列表组件 ArticleList
│ └── 文章详情组件 ArticleDetail
└── 底部组件 Footer
比如 App.vue 的 template 常见长这样:
<template>
<div id="app">
<Header />
<Sidebar />
<MainContent />
<Footer />
</div>
</template>
挂载应用
应用实例必须在调用了 .mount() 方法后才会渲染出来。该方法接收一个“容器”参数,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串:
<div id="app"></div>
app.mount('#app')
应用根组件的内容将会被渲染在容器元素里面。容器元素自己将不会被视为应用的一部分。
.mount() 方法应该始终在整个应用配置和资源注册完成后被调用。同时请注意,不同于其他资源注册方法,它的返回值是根组件实例而非应用实例。
简要来说:
app.mount('#app')的作用是把你用 Vue 创建的应用,挂载到页面上 id 为app的 DOM 元素中 你写的 Vue 组件只是JavaScript 对象,页面还看不到。 只有执行了app.mount('#app')Vue 才会接管这个 DOM 元素,并把你的组件渲染进去,最终出现在用户的浏览器里。
示例:
html:
<body>
<div id="app"></div> <!-- 占位容器 -->
</body>
js:
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App) // 创建一个 Vue 应用,传入根组件
app.mount('#app') // 把它挂载到 id 为 app 的 <div> 上
这时候,Vue 会自动:
- 找到
<div id="app"> - 用你的
App.vue的内容替换掉它 - 然后 Vue 就开始控制这个区域了(组件渲染、响应式数据更新、事件绑定,全都生效)
app.js 和 main.vue 的关系
App.vue是页面要显示的内容(也就是你的根组件),定义了你的应用界面的第一个组件(比如首页框架)main.js是程序入口,负责引导应用、加载App.vue并挂载到页面上,告诉 Vue “用哪个组件作为应用的开始”,然后挂到 HTML 页面上。
示例:
app.vue
<template>
<h1>Hello, World</h1>
</template>
<script>
export default {
name: 'App'
}
</script>
main.js
import { createApp } from 'vue'
import App from './App.vue' // 1. 把 App.vue 引进来
const app = createApp(App) // 2. 用 App.vue 创建一个 Vue 应用
app.mount('#app') // 3. 挂载到页面上的 #app 位置
index.html(通常是 public/index.html):
<div id="app"></div>
- 当页面打开时,
main.js运行,把App.vue里的<h1>Hello, World</h1>,- 插入到
<div id="app"></div>里面。
这样页面上你就能看到 "Hello, World"!
也就是说,App.vue 是内容,main.js 是启动器。
应用配置
应用实例会暴露一个 .config 对象允许我们配置一些应用级的选项,例如定义一个应用级的错误处理器,用来捕获所有子组件上的错误:
app.config.errorHandler = (err) => {
/* 处理错误 */
}
应用实例还提供了一些方法来注册应用范围内可用的资源,例如注册一个组件:
app.component('TodoDeleteButton', TodoDeleteButton)
这使得 TodoDeleteButton 在应用的任何地方都是可用的。我们会在指南的后续章节中讨论关于组件和其他资源的注册。你也可以在 API 参考中浏览应用实例 API 的完整列表。
确保在挂载应用实例之前完成所有应用配置!
简单来说:
- 你用
createApp(App)创建了一个Vue应用实例,就是app。 - 这个
app有一个.config属性,里面可以设置一些全局的配置。 - 你可以告诉 Vue —— “如果哪个组件里面出错了,应该怎么处理”。
app.config.errorHandler = (err) => {
/* 这里写出错以后要做的事情 */
}
- 以后只要任何一个组件出错了,Vue 会自动执行你这里写好的错误处理函数!
- 如果你设置了
app.config.errorHandler,Vue就不会让整个应用崩掉,而是调用你这里设置的函数,你可以在里面记录日志、弹个提示框,或者处理错误。
而app.component('TodoDeleteButton', TodoDeleteButton), 是全局注册组件
注册后,在整个应用的任何组件里,你都可以直接用 <TodoDeleteButton />,不用再单独引入。
示例:
- 假设我们有一个按钮组件:TodoDeleteButton.vue
<!-- src/components/TodoDeleteButton.vue -->
<template>
<button @click="handleClick">删除</button>
</template>
<script>
export default {
name: 'TodoDeleteButton',
methods: {
handleClick() {
alert('删除按钮被点击了!')
}
}
}
</script>
- 在
main.js注册成全局组件
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import TodoDeleteButton from './components/TodoDeleteButton.vue' // 引入组件
const app = createApp(App)
// 注册为全局组件
app.component('TodoDeleteButton', TodoDeleteButton)
app.mount('#app')
- 在任何地方直接使用(比如在
App.vue中用)
<!-- src/App.vue -->
<template>
<div>
<h1>任务列表</h1>
<!-- 直接使用,不需要 import -->
<TodoDeleteButton />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
多个应用实例
应用实例并不只限于一个。createApp API 允许你在同一个页面中创建多个共存的 Vue 应用,而且每个应用都拥有自己的用于配置和全局资源的作用域。
const app1 = createApp({
/* ... */
})
app1.mount('#container-1')
const app2 = createApp({
/* ... */
})
app2.mount('#container-2')
如果你正在使用 Vue 来增强服务端渲染 HTML,并且只想要 Vue 去控制一个大型页面中特殊的一小部分,应避免将一个单独的 Vue 应用实例挂载到整个页面上,而是应该创建多个小的应用实例,将它们分别挂载到所需的元素上去。
多个应用实例,主要是用在不是单页应用(SPA)的情况下,
需要局部增强功能、模块独立管理、或者渐进式接入 Vue的时候。
| 什么时候用多个应用? | 为什么? |
|---|---|
| 传统网页,局部用 Vue | 不需要重写整个页面 |
| 大型网站,不同模块独立 | 避免模块互相影响 |
| 多页面应用,每页单独挂载 | 每页加载自己的小应用 |
模板语法
Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有的 Vue 模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。
在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。
文本插值
最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法 (即双大括号):
<span>Message: {{ msg }}</span>
双大括号标签会被替换为相应组件实例中 msg 属性的值。同时每次 msg 属性更改时它也会同步更新。
原始HTML
双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,你需要使用 v-html 指令:
<p>Using text interpolation: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
这里我们遇到了一个新的概念。这里看到的 v-html attribute 被称为一个指令。指令由 v- 作为前缀,表明它们是一些由 Vue 提供的特殊 attribute,你可能已经猜到了,它们将为渲染的 DOM 应用特殊的响应式行为。这里我们做的事情简单来说就是:在当前组件实例上,将此元素的 innerHTML 与 rawHtml 属性保持同步。
span 的内容将会被替换为 rawHtml 属性的值,插值为纯 HTML——数据绑定将会被忽略。注意,你不能使用 v-html 来拼接组合模板,因为 Vue 不是一个基于字符串的模板引擎。在使用 Vue 时,应当使用组件作为 UI 重用和组合的基本单元。
Attribute 绑定
双大括号不能在 HTML attributes 中使用。想要响应式地绑定一个 attribute,应该使用 v-bind 指令:
<div v-bind:id="dynamicId"></div>
v-bind 指令指示* Vue 将元素的 id attribute 与组件的 dynamicId 属性保持一致*。如果绑定的值是 null 或者 undefined,那么该 attribute 将会从渲染的元素上移除。
简写
因为 v-bind 非常常用,我们提供了特定的简写语法:
<div :id="dynamicId"></div>
开头为 : 的 attribute 可能和一般的 HTML attribute 看起来不太一样,但它的确是合法的 attribute 名称字符,并且所有支持 Vue 的浏览器都能正确解析它。此外,他们不会出现在最终渲染的 DOM 中。简写语法是可选的,但相信在你了解了它更多的用处后,你应该会更喜欢它。
同名简写
如果 attribute 的名称与绑定的 JavaScript 值的名称相同,那么可以进一步简化语法,省略 attribute 值:
<!-- 与 :id="id" 相同 -->
<div :id></div>
<!-- 这也同样有效 -->
<div v-bind:id></div>
布尔型 Attribute
布尔型 attribute 依据 true / false 值来决定 attribute 是否应该存在于该元素上。disabled 就是最常见的例子之一。
v-bind 在这种场景下的行为略有不同:
<button :disabled="isButtonDisabled">Button</button>
当 isButtonDisabled 为真值或一个空字符串 (即 <button disabled="">) 时,元素会包含这个 disabled attribute。而当其为其他假值时 attribute 将被忽略。
动态绑定多个值
如果你有像这样的一个包含多个 attribute 的 JavaScript 对象:
const objectOfAttrs = {
id: 'container',
class: 'wrapper',
style: 'background-color:green'
}
通过不带参数的 v-bind,你可以将它们绑定到单个元素上:
<div v-bind="objectOfAttrs"></div>
使用 JavaScript 表达式
至此,我们仅在模板中绑定了一些简单的属性名。但是 Vue 实际上在所有的数据绑定中都支持完整的 JavaScript 表达式:
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
<div :id="`list-${id}`"></div>
这些表达式都会被作为 JavaScript ,以当前组件实例为作用域解析执行。
在 Vue 模板内,JavaScript 表达式可以被使用在如下场景上:
- 在文本插值中 (双大括号)
- 在任何 Vue 指令 (以
v-开头的特殊 attribute) attribute 的值中
仅支持表达式
每个绑定仅支持单一表达式,也就是一段能够被求值的 JavaScript 代码。一个简单的判断方法是是否可以合法地写在 return 后面。
因此,下面的例子都是无效的:
<!-- 这是一个语句,而非表达式 -->
{{ var a = 1 }}
<!-- 条件控制也不支持,请使用三元表达式 -->
{{ if (ok) { return message } }}
调用函数
可以在绑定的表达式中使用一个组件暴露的方法:
<time :title="toTitleDate(date)" :datetime="date"> {{ formatDate(date) }} </time>
组件暴露(Expose)就是指一个组件把它内部的某些属性、方法、状态等“公开”出来,供外部使用(例如父组件或其他调用者)。
示例:
vue3中通过 defineExpose 来显式“暴露”组件内部内容:
<script setup>
const doSomething = () => {
console.log("组件内部的方法");
}
// 暴露这个方法给父组件
defineExpose({
doSomething
})
</script>
这样,父组件就可以通过
ref拿到子组件实例,然后调用doSomething方法。
受限的全局访问
模板中的表达式将被沙盒化,仅能够访问到有限的全局对象列表。该列表中会暴露常用的内置全局对象,比如 Math 和 Date。
没有显式包含在列表中的全局对象将不能在模板内表达式中访问,例如用户附加在 window 上的属性。然而,你也可以自行在 app.config.globalProperties 上显式地添加它们,供所有的 Vue 表达式使用。
简单来说,Vue 的模板语法({{ }} 或 v-if="..." 等)中的表达式是**沙盒化(sandboxed)**的。
所谓沙盒化就是,当你在vue的模板中写这样的表达式:
{{ Math.random() }}
{{ window.alert("Hi") }}
Math.random()可以用:因为 Vue 明确允许。window.alert("Hi")不能用: 因为window不在 Vue 允许访问的范围里。
Vue 就是通过沙盒化模板表达式,防止你在模板中访问不安全的对象(比如 window, document, localStorage 等)。
为什么要沙盒化?
为了安全和清晰:
- 防止用户误用敏感对象(比如在模板里访问
window.localStorage) - 限制模板的职责(模板只关注展示逻辑)
- 减少被注入恶意代码的风险
打个比方:
模板沙盒就像是一个儿童游乐区,Vue 只给你放了一些“安全玩具”(比如 Math, Date, Boolean),其他东西(像 window, document, localStorage)都藏起来了,以免你“受伤”或者“搞破坏”。
需要注意的是虽然 Vue 的模板表达式本身是“沙盒化”的,模板只能访问有限的全局对象,但:
如果你在组件内部定义了一个函数(比如在
setup()或<script setup>里),这个函数是你自己的,它不是全局变量,它可以做任何合法的 JavaScript 操作,包括访问window.alert()。
<template>
<button @click="showAlert">点我</button>
</template>
<script setup>
function showAlert() {
window.alert("Hi from inside function!");
}
</script>
也就是说,Vue 的沙盒机制限制的是模板表达式本身访问全局对象的能力,但不限制你在组件方法或函数中访问全局对象,你想调用 window.alert、console.log、fetch 都可以。
理由有两个
- 因为Vue把模板设计为“只管展示”的部分,它的作用是:把数据渲染成 HTML。
-
模板表达式应当是简单、无副作用的(例如
user.name、Math.round(price)) -
不应当执行副作用代码(比如修改 DOM、弹窗、请求接口等)
- 提高安全性(防止 XSS 或滥用)
如果不加沙盒,用户可以在模板里写:
{{ window.location.href = 'http://malicious.com' }}
这将是一个严重的安全漏洞。
沙盒化可以有效防止模板中访问敏感的全局对象,**防止恶意注入、XSS(跨站脚本攻击)**等。
指令 Directives
指令是带有 v- 前缀的特殊 attribute。Vue 提供了许多内置指令,包括上面我们所介绍的 v-bind 和 v-html。
指令 attribute 的期望值为一个 JavaScript 表达式 (除了少数几个例外,即之后要讨论到的 v-for、v-on 和 v-slot)。一个指令的任务是在其表达式的值变化时响应式地更新 DOM。以 v-if 为例:
指令 attribute 的期望值为一个 JavaScript 表达式 (除了少数几个例外,即之后要讨论到的 v-for、v-on 和 v-slot)。一个指令的任务是在其表达式的值变化时响应式地更新 DOM。以 v-if 为例:
<p v-if="seen">Now you see me</p>
这里,v-if 指令会基于表达式 seen 的值的真假来移除/插入该 <p> 元素。
v-if和:disabled的区别:>
v-if是控制“要不要渲染出来”,而:disabled是控制“渲染出来但是否可用”。
v-if:是否渲染 DOM 元素
-
如果条件为 false,Vue 会完全从 DOM 中移除这个元素。
-
适用于 需要彻底隐藏或销毁组件/元素 的场景。
<button v-if="isLoggedIn">退出登录</button>
isLoggedIn = false时,页面上完全没有这个按钮。
:disabled: 是否禁用一个已经存在的表单元素
- 它是绑定原生 HTML 属性
disabled - 元素仍然在页面中,只是不可点击 / 不可输入
<button :disabled="!isFormValid">提交</button>
- 表单没填好时,按钮灰掉、不能点,但按钮还在页面上。
参数 Arguments
某些指令会需要一个“参数”,在指令名后通过一个冒号隔开做标识。例如用 v-bind 指令来响应式地更新一个 HTML attribute:
<a v-bind:href="url"> ... </a>
<!-- 简写 -->
<a :href="url"> ... </a>
这里 href 就是一个参数,它告诉 v-bind 指令将表达式 url 的值绑定到元素的 href attribute 上。在简写中,参数前的一切 (例如 v-bind:) 都会被缩略为一个 : 字符。
另一个例子是 v-on 指令,它将监听 DOM 事件:
<a v-on:click="doSomething"> ... </a>
<!-- 简写 -->
<a @click="doSomething"> ... </a>
这里的参数是要监听的事件名称:click。v-on 有一个相应的缩写,即 @ 字符。我们之后也会讨论关于事件处理的更多细节。
动态参数
同样在指令参数上也可以使用一个 JavaScript 表达式,需要包含在一对方括号内:
<!--
注意,参数表达式有一些约束,
参见下面“动态参数值的限制”与“动态参数语法的限制”章节的解释
-->
<a v-bind:[attributeName]="url"> ... </a>
<!-- 简写 -->
<a :[attributeName]="url"> ... </a>
这里的 attributeName 会作为一个 JavaScript 表达式被动态执行,计算得到的值会被用作最终的参数。举例来说,如果你的组件实例有一个数据属性 attributeName,其值为 "href",那么这个绑定就等价于 v-bind:href。
相似地,你还可以将一个函数绑定到动态的事件名称上:
<a v-on:[eventName]="doSomething"> ... </a>
<!-- 简写 -->
<a @[eventName]="doSomething"> ... </a>
在此示例中,当 eventName 的值是 "focus" 时,v-on:[eventName] 就等价于 v-on:focus。
动态参数中表达式的值应当是一个字符串,或者是 null。特殊值 null 意为显式移除该绑定。其他非字符串的值会触发警告。
动态参数表达式因为某些字符的缘故有一些语法限制,比如空格和引号,在 HTML attribute 名称中都是不合法的。例如下面的示例:
<!-- 这会触发一个编译器警告 -->
<a :['foo' + bar]="value"> ... </a>
如果你需要传入一个复杂的动态参数,我们推荐使用计算属性替换复杂的表达式,也是 Vue 最基础的概念之一,我们很快就会讲到。
当使用 DOM 内嵌模板 (直接写在 HTML 文件里的模板) 时,我们需要避免在名称中使用大写字母,因为浏览器会强制将其转换为小写:
<a :[someAttr]="value"> ... </a>
上面的例子将会在 DOM 内嵌模板中被转换为 :[someattr]。如果你的组件拥有 “someAttr” 属性而非 “someattr”,这段代码将不会工作。单文件组件内的模板不受此限制。
修饰符Modifiers
修饰符是以点开头的特殊后缀,表明指令需要以一些特殊的方式被绑定。例如 .prevent 修饰符会告知 v-on 指令对触发的事件调用 event.preventDefault():
<form @submit.prevent="onSubmit">...</form>
之后在讲到 v-on 和 v-model 的功能时,你将会看到其他修饰符的例子。
最后,在这里你可以直观地看到完整的指令语法:
响应式基础
声明响应状态
在组合式 API 中,推荐使用 ref() 函数来声明响应式状态:
import { ref } from 'vue'
const count = ref(0)
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(0)创建了一个响应式的“引用对象” -
count.value才是真正的值,Vue 会追踪它的变化并自动更新视图
每当你修改 count.value,Vue 会自动让相关模板更新。
ref
- 在组件逻辑中声明响应式数据
这是最常见的用法,尤其是在 Composition API(setup() 或 <script setup>)中。
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
ref(0)创建了一个响应式的“引用对象”count.value才是真正的值,Vue 会追踪它的变化并自动更新视图
每当你修改 count.value,Vue 会自动让相关模板更新。
在模板中可以直接用 .value 里的值:
<template>
<button @click="count++">{{ count }}</button>
</template>
<script setup>
const count = ref(0)
</script>
在模板中不用 .value,Vue 会自动“解包”。
- 获取 DOM 元素或组件实例的引用
你也可以给一个元素或组件加上 ref="xxx",然后在脚本里通过 ref 对象访问它。
<template>
<input ref="inputRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
inputRef.value.focus()
})
</script>
- 这时
inputRef.value是对应的 DOM 元素 - 你可以调用
.focus()、.scrollIntoView()等原生方法
ref 是 Vue 中用于声明响应式数据或获取 DOM/组件引用的工具,在 Composition API 中非常核心。
reactive 适用于多个字段,
import { reactive } from 'vue'
const state = reactive({
count: 0,
name: '椰椰'
})
- 模板中可以写
{{ state.count }} - 只适合对象或数组,不适合单个基本类型(如数字、字符串)
<script setup>
在 setup() 函数中手动暴露大量的状态和方法非常繁琐。幸运的是,我们可以通过使用单文件组件 (SFC) 来避免这种情况。我们可以使用 <script setup> 来大幅度地简化代码:
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">
{{ count }}
</button>
</template>
<script setup> 中的顶层的导入、声明的变量和函数可在同一组件的模板中直接使用。你可以理解为模板是在同一作用域内声明的一个 JavaScript 函数——它自然可以访问与它一起声明的所有内容。
<script setup>是 Vue 3 中 Composition API 的语法糖(简化写法) ,它的作用是让你用更简洁、直观的方式编写组件逻辑。你可以理解成是对下面这种传统写法的封装和优化
传统写法:
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return { count }
}
}
</script>
<script setup> 写法(推荐):
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
两者效果完全一样,但 <script setup> 不需要手动写 setup() 函数、不需要手动 return 数据,Vue 会自动帮你处理。
为什么要使用ref()
你可能会好奇:为什么我们需要使用带有 .value 的 ref,而不是普通的变量?为了解释这一点,我们需要简单地讨论一下** Vue 的响应式系统是如何工作的**。
当你在模板中使用了一个 ref,然后改变了这个 ref 的值时,Vue 会自动检测到这个变化,并且相应地更新 DOM。这是通过一个基于依赖追踪的响应式系统实现的。当一个组件首次渲染时,Vue 会追踪在渲染过程中使用的每一个 ref。然后,当一个 ref 被修改时,它会触发追踪它的组件的一次重新渲染。
在标准的 JavaScript 中,检测普通变量的访问或修改是行不通的。然而,我们可以通过 getter 和 setter 方法来拦截对象属性的 get 和 set 操作。
该 .value 属性给予了 Vue 一个机会来检测 ref 何时被访问或修改。在其内部,Vue 在它的 getter 中执行追踪,在它的 setter 中执行触发。从概念上讲,你可以将 ref 看作是一个像这样的对象:
// 伪代码,不是真正的实现
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}
另一个 ref 的好处是,与普通变量不同,你可以将 ref 传递给函数,同时保留对最新值和响应式连接的访问。当将复杂的逻辑重构为可重用的代码时,这将非常有用。
简单来说:ref 不仅是响应式的,它还可以 像普通变量一样传来传去(比如传进函数) ,而且它的响应式特性 不会丢失。这在你拆分代码逻辑、封装通用函数时非常有用。
普通变量如果你传给函数,传的只是一个值(拷贝),不是响应式引用。比如这样:
let count = 0
function increase(val) {
val++
}
increase(count)
console.log(count) // 还是 0,因为 val 是 count 的拷贝
ref 的行为:传的是“响应式对象”,引用还在
import { ref } from 'vue'
const count = ref(0)
function increase(refVal) {
refVal.value++
}
increase(count)
console.log(count.value) // 是 1!响应式对象被直接修改
refVal是一个“引用对象”- 修改
refVal.value,等于修改原始的count.value - 响应式状态没丢,UI 会自动更新
深层响应性
Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map。
Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到
import { ref } from 'vue'
const obj = ref({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
// 以下都会按照期望工作
obj.value.nested.count++
obj.value.arr.push('baz')
}
非原始值将通过 reactive() 转换为响应式代理,该函数将在后面讨论。
也可以通过 shallow ref 来放弃深层响应性。对于浅层 ref,只有 .value 的访问会被追踪。浅层 ref 可以用于避免对大型数据的响应性开销来优化性能、或者有外部库管理其内部状态的情况。
简单来说,> 用 ref 包起来的值,不管是简单的数字,还是很复杂的对象、数组、Map,都能被 Vue 自动“监听”变化,并自动更新界面。
dom更新时机
当你修改了响应式状态时,DOM 会被自动更新。但是需要注意的是,DOM 更新不是同步的。Vue 会在“next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。
要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API:
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// 现在 DOM 已经更新了
}
reactive()
还有另一种声明响应式状态的方式,即使用 reactive() API。与将内部值包装在特殊对象中的 ref 不同,reactive() 将使对象本身具有响应性:
import { reactive } from 'vue'
const state = reactive({ count: 0 })
在模板中使用:
<button @click="state.count++"> {{ state.count }} </button>
响应式对象是 JavaScript 代理,其行为就和普通对象一样。不同的是,Vue 能够拦截对响应式对象所有属性的访问和修改,以便进行依赖追踪和触发更新。
reactive() 将深层地转换对象:当访问嵌套对象时,它们也会被 reactive() 包装。当 ref 的值是一个对象时,ref() 也会在内部调用它。与浅层 ref 类似,这里也有一个 shallowReactive() API 可以选择退出深层响应性。
深层响应式: 如果你这样写:
const user = ref({ name: '小明' }) user.value.name = '小红'这里
user.value是个对象,Vue 会自动用reactive()包裹这个对象,所以你改name,Vue 也能检测并更新界面(这叫“深层响应式”)。
如果你不想让 Vue 自动处理里面的嵌套对象或嵌套 ref,而是只想监听最外层变化,可以用
shallowRef:
const user = shallowRef({
name: ref('小明')
})
user.value.name.value = '小红' // ✅ 这个 ref 保留了,不会被“解包”
适合用在性能敏感或你自己想手动控制响应的场景。
ref和reactive的区别
ref用来包基本类型或任意值,访问时要.value
reactive用来包对象或数组,直接使用属性,不用.value
| 特性 | ref | reactive |
|---|---|---|
| 包装的类型 | 任意(值/对象/数组/Map 都行) | 必须是对象(数组、对象、Map 等) |
获取值是否需要 .value | ✅ 需要(必须) | ❌ 不需要 |
| 是否深度响应式 | ✅ 是 | ✅ 是 |
| 常用于 | 基本类型(数值、字符串、布尔值) | 复杂结构(多个字段、数组、嵌套对象) |
| 是否可解构 | ❌ 解构会失去响应性 | ✅ 解构后响应性还在 |
| 适合组合式 API | ✅ 非常适合 | ✅ 非常适合 |
示例:
ref:
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
reactive
import { reactive } from 'vue'
const user = reactive({
name: '小明',
age: 18
})
console.log(user.name) // 小明
user.age++ // 自动响应,界面会更新
也可以用ref包对象:
const user = ref({ name: '小明' })
user.value.name = '小红' // ✅ 会触发响应式更新
总结:
ref(基本类型) 推荐
ref(对象) 可以,但要 .value
reactive(对象) 推荐
reactive(基本类型) 不支持,会报错
Reactive Proxy vs. Original
值得注意的是,reactive() 返回的是一个原始对象的 Proxy,它和原始对象是不相等的:
const raw = {}
const proxy = reactive(raw)
// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false
只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是仅使用你声明对象的代理版本。
为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身:
// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true
这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理:
const proxy = reactive({})
const raw = {}
proxy.nested = raw
console.log(proxy.nested === raw) // false
reactive() 的局限性
- 有限的值类型:它只能用于对象类型 (对象、数组和如
Map、Set这样的[集合类型](developer.mozilla.org/en-US/docs/… - 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:
let state = reactive({ count: 0 })
// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失!)
state = reactive({ count: 1 })
(就是不能让state指向新的对象,不然还是会监听旧对象,新对象的变化不会触发更新)
- 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:
const state = reactive({ count: 0 })
// 当解构时,count 已经与 state.count 断开连接
let { count } = state
// 不会影响原始的 state
count++
// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
// 我们必须传入整个对象以保持响应性
callSomeFunction(state.count)
由于这些限制,我们建议使用 ref() 作为声明响应式状态的主要 API。
额外的 ref 解包细节
作为 reactive 对象的属性
一个 ref 会在作为响应式对象的属性被访问或修改时自动解包。换句话说,它的行为就像一个普通的属性:
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
如果将一个新的 ref 赋值给一个关联了已有 ref 的属性,那么它会替换掉旧的 ref:
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// 原始 ref 现在已经和 state.count 失去联系
console.log(count.value) // 1
只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包。
数组和集合的注意事项
与 reactive 对象不同的是,当 ref 作为响应式数组或原生集合类型 (如 Map) 中的元素被访问时,它不会被解包:
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
在模板中解包的注意事项
在模板渲染上下文中,只有顶级的 ref 属性才会被解包。
在下面的例子中,count 和 object 是顶级属性,但 object.id 不是:
const count = ref(0)
const object = { id: ref(1) }
简单来说,上面两个属性在模板中使用时:
<p>{{ count }}</p> <!-- 自动解包,等同于 count.value -->
<p>{{ object.id }}</p> <!-- 不会自动解包,还是一个 ref -->
-
count是顶级变量(直接定义并返回),所以它在模板中会被自动解包。 -
object.id是一个嵌套在object对象里的 ref,不是顶级变量,所以在模板中不会自动解包,要写成:
<p>{{ object.id.value }}</p>
条件渲染
v-if
v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。
<h1 v-if="awesome">Vue is awesome!</h1>
v-else
你也可以使用 v-else 为 v-if 添加一个“else 区块”。
<button @click="awesome = !awesome">Toggle</button>
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>
一个 v-else 元素必须跟在一个 v-if 或者 v-else-if 元素后面,否则它将不会被识别。
v-else-if
顾名思义,v-else-if 提供的是相应于 v-if 的“else if 区块”。它可以连续多次重复使用:
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
和 v-else 类似,一个使用 v-else-if 的元素必须紧跟在一个 v-if 或一个 v-else-if 元素后面。
<template> 上的 v-if
因为 v-if 是一个指令,他必须依附于某个元素。但如果我们想要切换不止一个元素呢?在这种情况下我们可以在一个 <template> 元素上使用 v-if,这只是一个不可见的包装器元素,最后渲染的结果并不会包含这个 <template> 元素。
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
v-else 和 v-else-if 也可以在 <template> 上使用。
v-show
另一个可以用来按条件显示一个元素的指令是 v-show。其用法基本一样:
<h1 v-show="ok">Hello!</h1>
不同之处在于 v-show 会在 DOM 渲染中保留该元素;v-show 仅切换了该元素上名为 display 的 CSS 属性。
v-show 不支持在 <template> 元素上使用,也不能和 v-else 搭配使用。
v-if vs. v-show
v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。
v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。
相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换。
总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。
列表渲染
v-for
我们可以使用 v-for 指令基于一个数组来渲染一个列表。v-for 指令的值需要使用 item in items 形式的特殊语法,其中 items 是源数据的数组,而 item 是迭代项的别名:
const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
<li v-for="item in items">
{{ item.message }}
</li>
在 v-for 块中可以完整地访问父作用域内的属性和变量。v-for 也支持使用可选的第二个参数表示当前项的位置索引。
const parentMessage = ref('Parent')
const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
<li v-for="(item, index) in items">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
Parent - 0 - Foo
Parent - 1 - Bar
v-for 变量的作用域和下面的 JavaScript 代码很类似:
const parentMessage = 'Parent'
const items = [
/* ... */
]
items.forEach((item, index) => {
// 可以访问外层的 `parentMessage`
// 而 `item` 和 `index` 只在这个作用域可用
console.log(parentMessage, item.message, index)
})
注意 v-for 是如何对应 forEach 回调的函数签名的。实际上,你也可以在定义 v-for 的变量别名时使用解构,和解构函数参数类似:
<li v-for="{ message } in items">
{{ message }}
</li>
<!-- 有 index 索引时 -->
<li v-for="({ message }, index) in items">
{{ message }} {{ index }}
</li>
对于多层嵌套的 v-for,作用域的工作方式和函数的作用域很类似。每个 v-for 作用域都可以访问到父级作用域:
<li v-for="item in items">
<span v-for="childItem in item.children">
{{ item.message }} {{ childItem }}
</span>
</li>
v-for 与对象
你也可以使用 v-for 来遍历一个对象的所有属性。遍历的顺序会基于对该对象调用 Object.values() 的返回值来决定。
const myObject = reactive({
title: 'How to do lists in Vue',
author: 'Jane Doe',
publishedAt: '2016-04-10'
})
<ul>
<li v-for="value in myObject">
{{ value }}
</li>
</ul>
渲染结果:
<ul>
<li>How to do lists in Vue</li>
<li>Jane Doe</li>
<li>2016-04-10</li>
</ul>
可以通过提供第二个参数表示属性名 (例如 key):
<li v-for="(value, key) in myObject">
{{ key }}: {{ value }}
</li>
第三个参数表示位置索引:
<li v-for="(value, key, index) in myObject">
{{ index }}. {{ key }}: {{ value }}
</li>
通过 key 管理状态
Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。
默认模式是高效的,但只适用于列表渲染输出的结果不依赖子组件状态或者临时 DOM 状态 (例如表单输入值) 的情况。
为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key attribute:
<div v-for="item in items" :key="item.id">
<!-- 内容 -->
</div>
当你使用 <template v-for> 时,key 应该被放置在这个 <template> 容器上:
<template v-for="todo in todos" :key="todo.name">
<li>{{ todo.name }}</li>
</template>