vue官方文档中译中

246 阅读32分钟

简介

组合式api vs 选项式api

  1. 写法结构不同

选项式 API(Options API)
用各种「选项」组织代码,比如 datamethodscomputedwatch,每个功能点有各自固定的位置。

<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>

  1. 响应式实现方式不同
  • 选项式:Vue 自动帮你把 data 里的数据变成响应式的(底层是用 Object.defineProperty 或 Proxy)。

  • 组合式:要手动用 ref()reactive() 创建响应式数据。

  1. 逻辑复用方式不同
  • 选项式:通常靠 mixins,但是 mixins 很容易命名冲突、来源不明确。

mixins是什么 在 Vue(选项式 API)里,mixin(混入)就是一个可以被多个组件共享的对象。
这个对象里可以写 datamethodscomputedwatch 等,就像正常组件里写的一样。 然后组件通过 mixins: [] 把它们「混进来」,实现逻辑复用。 如果 mixin 和组件都定义了同名的 datamethods,容易搞混是谁定义的。

  • 组合式:可以直接封装成自定义的 逻辑函数(叫 composables,比如 useXXX()),清晰且可组合。
  1. 维护难易
  • 选项式:一个组件里,不同功能点分散在各个选项块里(比如 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.vuetemplate 常见长这样:

<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 />,不用再单独引入。

示例:

  1. 假设我们有一个按钮组件:TodoDeleteButton.vue
<!-- src/components/TodoDeleteButton.vue -->
<template>
  <button @click="handleClick">删除</button>
</template>

<script>
export default {
  name: 'TodoDeleteButton',
  methods: {
    handleClick() {
      alert('删除按钮被点击了!')
    }
  }
}
</script>

  1. 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')
  1. 在任何地方直接使用(比如在 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.alertconsole.logfetch 都可以。

理由有两个

  1. 因为Vue把模板设计为“只管展示”的部分,它的作用是:把数据渲染成 HTML
  • 模板表达式应当是简单、无副作用的(例如 user.nameMath.round(price)

  • 不应当执行副作用代码(比如修改 DOM、弹窗、请求接口等)

  1. 提高安全性(防止 XSS 或滥用)

如果不加沙盒,用户可以在模板里写:

{{ window.location.href = 'http://malicious.com' }}

这将是一个严重的安全漏洞

沙盒化可以有效防止模板中访问敏感的全局对象,**防止恶意注入、XSS(跨站脚本攻击)**等。

指令 Directives

指令是带有 v- 前缀的特殊 attribute。Vue 提供了许多内置指令,包括上面我们所介绍的 v-bind 和 v-html

指令 attribute 的期望值为一个 JavaScript 表达式 (除了少数几个例外,即之后要讨论到的 v-forv-on 和 v-slot)。一个指令的任务是在其表达式的值变化时响应式地更新 DOM。以 v-if 为例:

指令 attribute 的期望值为一个 JavaScript 表达式 (除了少数几个例外,即之后要讨论到的 v-forv-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>

这里的参数是要监听的事件名称:clickv-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 的功能时,你将会看到其他修饰符的例子。

最后,在这里你可以直观地看到完整的指令语法:

image.png

响应式基础

声明响应状态

在组合式 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

  1. 在组件逻辑中声明响应式数据

这是最常见的用法,尤其是在 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 会自动“解包”。

  1. 获取 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

特性refreactive
包装的类型任意(值/对象/数组/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() 的局限性

  1. 有限的值类型:它只能用于对象类型 (对象、数组和如 MapSet 这样的[集合类型](developer.mozilla.org/en-US/docs/…
  2. 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:
let state = reactive({ count: 0 })

// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失!)
state = reactive({ count: 1 })

就是不能让state指向新的对象,不然还是会监听旧对象,新对象的变化不会触发更新

  1. 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:
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>