第10章 10.1 Vue 3 新特性展望

📖 这是「Vue3 从入门到精通」系列第 44 / 45

🎯 开场 3 分钟:为什么要学这个?

上一章我们钻进了 Vue 3 编译器的「黑箱」,看到了 .vue 文件是怎么变成 JavaScript 代码的。

但你有没有想过:如果编译器能再快一点,如果响应式能更「懒」一点,如果写法能更简洁一点……

Vue 团队其实一直在想这些问题。这几年社区里经常听到几个词:Vapor Mode、defineModel、还有拿 Vue 和 Solid 对比的声音。

这一章我们就来聊聊这些「新特性」到底是个啥,它们解决的是什么问题,以及你有没有必要现在就学。

学完这章,你会:
- 搞清楚 Vapor Mode 是什么、什么时候能用
- 知道 defineModel 怎么用、什么时候该用它
- 能看懂 Vue 和 Solid 的核心区别,不会被社区争论带节奏


🧱 基础 25 分钟:核心概念(小白视角)

10.1.1 什么是 Vapo\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\nr Mode?

是什么?

Vapor Mode 是 Vue 团队正在开发的一种全新的编译输出模式

我们先回忆一下 Vue 3 现在是怎么工作的:

.vue 文件 → 编译器 → render 函数 → 虚拟 DOM → 真实 DOM

虚拟 DOM 这一步是 Vue 2 时代的设计,它好处是跨平台(你能用同样的代码输出 Native App、Web、SSR),坏处是多了一层性能损耗

Vapor Mode 的思路是:既然浏览器性能已经很强了,不如直接把 .vue 编译成高性能的 DOM 操作代码,跳过虚拟 DOM 这一层。

编译出来的东西大概长这样:

// 传统模式:生成 render 函数
function render(ctx) {
return h('div', null, ctx.message)
}

// Vapor Mode:直接生成 DOM 操作
element.textContent = ctx.message

为什么要用?

想象你要搬家,现在有两种方式:
- 虚拟 DOM 方式:把所有东西打包 → 运到新地方 → 再拆包摆放(灵活但慢)
- Vapor 方式:预约搬家公司的「日式服务」,他们直接帮你一件件搬过去,不打包(快但失去跨平台能力)

Vapor 就是后面这种,追求极致性能,但代价是不能输出到 Native App 了。

怎么用?(目前状态)

⚠️ Vapor Mode 目前还在实验阶段,不能用于生产。Vue 官方说预计 2025 年会有更稳定版本。

但我们可以先看看它长什么样:

// Vapor 模式下的组件(编译后大概是这样)
import { ref, computed } from 'vue'

// defineModel 会得到直接操作 DOM 的能力
const message = defineModel('message', { default: 'Hello' })

// 编译后直接是 DOM 操作,没有虚拟 DOM
element.textContent = message.value

10.1.2 defineModel 是什么?

是什么?

defineModel 是 Vue 3.4 引入的一个新的响应式 API,专门用来简化「双向绑定」的写法。

我们先回忆一下以前怎么写 v-model

<script setup>
import { ref } from 'vue'

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

function onInput(e) {
emit('update:modelValue', e.target.value)
}
</script>

<template>
<input :value="modelValue" @input="onInput" />
</template>

用了 defineModel 之后:

<script setup>
// 一行搞定,不需要手动 defineProps 和 defineEmits
const model = defineModel()
</script>

<template>
<!-- 直接用 v-model,不需要 :value + @input -->
<input v-model="model" />
</template>

为什么要用?

就像你去快递站寄件:
- 以前:自己打包 → 填单 → 交给快递员(手动,一堆步骤)
- 现在:快递员直接拿专用袋装好,你填个单就走(defineModel,封装好了)

defineModel 把 props + emits 的模板代码封装成了一个「模型」,让你直接用。

怎么用?

<script setup>
// 基础用法:一个 model
const username = defineModel()

// 带选项:默认值、校验
const age = defineModel('age', { 
default: 18,
type: Number 
})
</script>

<template>
<input v-model="username" />
<input v-model="age" type="number" />
</template>

父组件这样用:

<!-- 基础用法 -->
<Child v-model="parentUsername" />

<!-- 带参数的用法 -->
<Child v-model:age="parentAge" />

10.1.3 Vue 3 和 Solid 到底有什么区别?

是什么?

Solid 是一个和 Vue 3 同时期出现的响应式框架,社区经常拿它们对比。

先看一个 Solid 的例子:

import { createSignal } from 'solid-js'

function Counter() {
const [count, setCount] = createSignal(0)

return (
<button onClick={() => setCount(count() + 1)}>
  Count: {count()}
</button>
)
}

再看 Vue 3:

<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
<button @click="count++">
Count: {{ count }}
</button>
</template>

核心区别在哪里?

对比项 Vue 3 Solid
响应式原理 依赖追踪(proxy) 追踪 + 编译时优化
写法 模板 + setup JSX
性能 更好(编译时优化更多)
入门难度 低(模板友好) 高(需要懂 JSX)

简单来说:
- Vue 3 像「自动挡汽车」—— 你踩油门就走,但车里有些零件是通用的,不是专门为这辆车设计的
- Solid 像「赛车」—— 每一零件都是定制的,性能更强,但需要你会驾驶

10.1.4 其他值得期待的新特性

新的 TypeScript 语法支持

Vue 3.5+ 对 TypeScript 的支持更好了,类型推导更精准:

<script setup lang="ts">
// 3.4 版本
const props = defineProps<{ msg: string }>()

// 3.5+ 版本可以更简洁
interface Props {
msg: string
count?: number
}
const { msg, count = 0 } = defineProps<Props>()
</script>

更快的编译速度

Vue 团队一直在优化编译器,Vapor Mode 成熟后,编译速度会明显提升。


🔥 实战 35 分钟:3 个递进的小项目

项目 1:体验 defineModel 的简洁(5 分钟)

场景:做一个最简单的计数器组件,感受 defineModel 的写法。

<!-- Counter.vue -->
<script setup>
const count = defineModel('count', { default: 0 })
</script>

<template>
<div class="counter">
<h2>计数器:{{ count }}</h2>
<button @click="count++">+1</button>
<button @click="count--">-1</button>
</div>
</template>

<style scoped>
.counter {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
button {
margin: 0 5px;
padding: 5px 15px;
}
</style>

父组件使用:

<!-- App.vue -->
<script setup>
import { ref } from 'vue'
import Counter from './Counter.vue'

const myCount = ref(10)
</script>

<template>
<Counter v-model:count="myCount" />
<p>父组件中的值:{{ myCount }}</p>
</template>

预期输出:点击按钮,两个组件的数字会同步变化。

解释:defineModel 把 propsemit 封装成了一行,父组件通过 v-model:count 传递初始值,子组件修改会自动同步回去。


项目 2:从 JSON 读取配置,动态切换主题(15 分钟)

场景:做一个「主题切换器」,主题配置从 JSON 文件读取,用户可以随时切换。

首先创建主题配置文件:

// themes.json
{
"themes": [
{
  "name": "默认蓝",
  "primary": "#1890ff",
  "background": "#ffffff",
  "text": "#000000"
},
{
  "name": "暗黑模式",
  "primary": "#1db954",
  "background": "#121212",
  "text": "#ffffff"
},
{
  "name": "护眼绿",
  "primary": "#2e7d32",
  "background": "#f5f5dc",
  "text": "#333333"
}
]
}

然后是主题切换组件:

<!-- ThemeSwitcher.vue -->
<script setup>
import { ref, watch } from 'vue'

// 模拟从 API/文件读取的配置
const themes = ref([
{ name: "默认蓝", primary: "#1890ff", background: "#ffffff", text: "#000000" },
{ name: "暗黑模式", primary: "#1db954", background: "#121212", text: "#ffffff" },
{ name: "护眼绿", primary: "#2e7d32", background: "#f5f5dc", text: "#333333" }
])

const currentTheme = defineModel('theme')
const selectedIndex = ref(0)

// 监听切换,应用新主题
watch(selectedIndex, (newIndex) => {
currentTheme.value = themes.value[newIndex]
applyTheme(themes.value[newIndex])
})

function applyTheme(theme) {
document.body.style.setProperty('--primary', theme.primary)
document.body.style.setProperty('--bg', theme.background)
document.body.style.setProperty('--text', theme.text)
}
</script>

<template>
<div class="theme-switcher">
<h3>选择主题:{{ themes[selectedIndex].name }}</h3>
<div class="theme-list">
  <button
    v-for="(theme, index) in themes"
    :key="theme.name"
    :class="{ active: selectedIndex === index }"
    @click="selectedIndex = index"
    :style="{ background: theme.background, color: theme.text, borderColor: theme.primary }"
  >
    {{ theme.name }}
  </button>
</div>
</div>
</template>

<style scoped>
.theme-switcher {
padding: 20px;
}
.theme-list {
display: flex;
gap: 10px;
}
button {
padding: 10px 20px;
border: 2px solid;
cursor: pointer;
}
button.active {
box-shadow: 0 0 10px var(--primary, #1890ff);
}
</style>

父组件:

<!-- App.vue -->
<script setup>
import { ref } from 'vue'
import ThemeSwitcher from './ThemeSwitcher.vue'

const theme = ref({ name: "默认蓝", primary: "#1890ff", background: "#ffffff", text: "#000000" })
</script>

<template>
<ThemeSwitcher v-model:theme="theme" />
<div class="demo-box">
<button>按钮</button>
<p>这是一段文字</p>
</div>
</template>

<style>
:root {
--primary: #1890ff;
--bg: #ffffff;
--text: #000000;
}
body {
background: var(--bg);
color: var(--text);
}
.demo-box button {
background: var(--primary);
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
}
</style>

预期输出:点击不同主题按钮,整个页面背景、文字颜色会随之变化。

解释:defineModel 让子组件可以直接修改父组件的 theme 对象,结合 watch 监听变化后动态修改 CSS 变量。


项目 3:做一个「待办清单持久化工具」(15 分钟)

场景:结合前两个项目,做一个带数据持久化的待办清单,数据保存到 localStorage。

<!-- TodoList.vue -->
<script setup>
import { ref, watch, onMounted } from 'vue'

// defineModel 实现双向绑定
const todos = defineModel('todos')
const inputText = ref('')

// 初始化时从 localStorage 读取
onMounted(() => {
const saved = localStorage.getItem('my-todos')
if (saved) {
todos.value = JSON.parse(saved)
} else {
todos.value = []
}
})

// 监听变化,自动保存
watch(todos, (newTodos) => {
localStorage.setItem('my-todos', JSON.stringify(newTodos))
}, { deep: true })

function addTodo() {
if (!inputText.value.trim()) return
todos.value.push({
id: Date.now(),
text: inputText.value,
done: false
})
inputText.value = ''
}

function toggleTodo(id) {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.done = !todo.done
}

function deleteTodo(id) {
todos.value = todos.value.filter(t => t.id !== id)
}
</script>

<template>
<div class="todo-list">
<h2>我的待办清单 ({{ todos.length }})</h2>

<div class="input-row">
  <input 
    v-model="inputText" 
    @keyup.enter="addTodo"
    placeholder="输入待办事项..."
  />
  <button @click="addTodo">添加</button>
</div>

<ul class="todos">
  <li 
    v-for="todo in todos" 
    :key="todo.id"
    :class="{ done: todo.done }"
  >
    <input 
      type="checkbox" 
      :checked="todo.done"
      @change="toggleTodo(todo.id)"
    />
    <span>{{ todo.text }}</span>
    <button class="delete" @click="deleteTodo(todo.id)">删除</button>
  </li>
</ul>

<p v-if="todos.length === 0" class="empty">暂无待办,添加一个吧!</p>
</div>
</template>

<style scoped>
.todo-list {
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
.input-row {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
input[type="text"] {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 10px 20px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button.delete {
background: #ff4d4f;
padding: 5px 10px;
}
.todos {
list-style: none;
padding: 0;
}
.todos li {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-bottom: 1px solid #eee;
}
.todos li.done span {
text-decoration: line-through;
color: #999;
}
.empty {
text-align: center;
color: #999;
padding: 20px;
}
</style>

父组件:

<!-- App.vue -->
<script setup>
import { ref } from 'vue'
import TodoList from './TodoList.vue'

const todoList = ref([])
</script>

<template>
<TodoList v-model:todos="todoList" />
</template>

预期输出
1. 刷新页面后,之前添加的待办还在
2. 勾选待办会添加删除线
3. 点击删除会移除该项

解释:结合 defineModel 实现组件间数据共享,使用 watch + localStorage 实现数据持久化。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:defineModel 的默认值不生效

错误示例

<script setup>
// 默认值只在 props 层面设置,如果父组件传了空值,会覆盖默认值
const count = defineModel('count', { default: 0 })
// 如果父组件这样用:<Child v-model:count="undefined" />
// count 可能是 undefined 而不是 0
</script>

正确示例

<script setup>
// 显式处理空值情况
const count = defineModel('count', { 
default: 0,
type: Number 
})

// 或者在模板里加兜底
// <span>{{ count ?? 0 }}</span>
</script>

坑 2:watch 监听 defineModel 变化的时机

错误示例

<script setup>
const model = defineModel()

// 在 watch 里直接修改 model,可能导致死循环
watch(model, (newVal) => {
if (newVal > 100) {
model.value = 100  // ⚠️ 死循环!
}
})
</script>

正确示例

<script setup>
const model = defineModel()

// 加 immediate: false 确保只在外部变化时触发
// 如果要限制范围,在 setter 里处理
const modelWithLimit = defineModel({
set(val) {
return Math.min(100, Math.max(0, val))  // 限制范围
}
})
</script>

坑 3:Vapor Mode 还没准备好就用

错误示例

// 看到 Vapor Mode 的介绍就去配置项目
// vite.config.js
export default {
compilerOptions: {
vapor: true  // ⚠️ 这不是有效配置!
}
}

正确示例

// 等官方正式发布后再使用
// 目前只能用实验性脚手架体验
// 关注 Vue 官方博客获取最新消息

坑 4:在 defineModel 里放复杂对象

错误示例

<script setup>
// 把整个表单对象放进去
const form = defineModel('form')
// 父组件:v-model:form="myForm"
// 问题:如果 form 里有嵌套对象,deep 监听会很慢
</script>

正确示例

<script setup>
// 拆分独立字段
const username = defineModel('username')
const email = defineModel('email')
const password = defineModel('password')

// 如果确实需要对象,用 shallowRef
import { shallowRef } from 'vue'
const form = shallowRef({ username: '', email: '', password: '' })
</script>

坑 5:拿 Vue 和 Solid 对比时忽视场景

错误理解

「Solid 比 Vue 快,所以 Solid 更好,Vue 完蛋了」

正确理解

  • 如果你做企业级后台,Vue 的生态和社区支持更重要
  • 如果你做高性能活动页,Solid 的编译时优化更合适
  • 没有银弹,选合适的工具

性能小贴士:用 shallowRef 减少响应式开销

<script setup>
import { ref, shallowRef } from 'vue'

// 普通 ref:递归监听,修改深层属性也会触发更新
const normal = ref({ user: { profile: { name: '' } } })

// shallowRef:只监听第一层,适合大数据量场景
const optimized = shallowRef({ user: { profile: { name: '' } } })

// 修改深层属性
normal.value.user.profile.name = 'test'  // 触发更新
optimized.value.user.profile.name = 'test'  // 不触发!需要整体替换
optimized.value = { user: { profile: { name: 'test' } } }  // 触发更新
</script>

调试技巧:console.log 也能看响应式

<script setup>
import { ref, watch } from 'vue'

const count = ref(0)

// 方式1:直接打印
console.log('count:', count.value)

// 方式2:watch 打印变化
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变成 ${newVal}`)
}, { immediate: true })

// 方式3:computed 打印
import { computed } from 'vue'
const doubled = computed(() => {
console.log('计算 doubled')
return count.value * 2
})
</script>

✏️ 练习题 + 作业题

练习题(5 道,10 分钟内完成)

练习 1(1 分钟):改默认值
- 输入:在项目 1 的 Counter 组件中,把默认值改成 100
- 预期输出:页面加载时显示 100
- 提示:defineModel 的第二个参数

练习 2(2 分钟):加一个重置按钮
- 输入:在项目 1 中加一个「重置到 0」的按钮
- 预期输出:点击后 count 变成 0
- 提示:给按钮加 @click 事件

练习 3(2 分钟):处理新主题配置
- 输入:给 themes.json 加一个「新年红」主题(primary: #dc143c, background: #fff5f5)
- 预期输出:下拉列表里能看到第 4 个选项
- 提示:直接在 themes.value.push() 或改 JSON

练习 4(3 分钟):把项目和持久化结合
- 输入:在项目 2 的主题切换器里,加入 localStorage 持久化
- 预期输出:刷新页面后保持上次选择的主题
- 提示:watch 配合 localStorage.setItem

练习 5(2 分钟):看图找错
- 输入:以下代码有什么问题?

<script setup>
const model = defineModel()
watch(model, (val) => {
model.value = val + 1  // 这行会怎样?
})
</script>
  • 预期输出:说出问题原因
  • 提示:watch 回调里修改 model 会怎样?

作业题(30 分钟 - 2 小时)

作业:做一个「读书笔记管理工具」

  • 需求描述:做一个可以管理读书笔记的页面,每条笔记包含书名、笔记内容、评分,评分用星星显示

  • 功能点
    1. 添加新笔记(输入书名 + 内容 + 选择 1-5 星)
    2. 展示笔记列表
    3. 用 defineModel 实现数据共享
    4. 用 localStorage 持久化(加分项:刷新不丢数据)

  • 加分项
    1. 可以删除单条笔记
    2. 可以按评分筛选笔记

  • 验收标准

  • 能跑起来,不报错
  • 添加笔记后列表显示正确
  • 刷新页面数据还在

  • 提交方式:评论区贴代码或 GitHub 链接


📚 总结 + 资源

这一章我们学了 3 个核心点:
1. Vapor Mode 是 Vue 新的编译目标,主打极致性能(但还在实验)
2. defineModel 简化了 v-model 的模板代码,一行替代原来的 props + emits
3. Vue 和 Solid 各有优势,选工具要看场景

延伸学习资源

  1. Vue 3 官方文档 - defineModel (英文原版,最准确)
  2. Vue 3 Vapor Mode RFC (英文,社区讨论)
  3. Solid.js 官方文档 (想了解 Solid 对比可以看)

互动钩子:你在项目里用过 defineModel 吗?感觉它比传统的 props + emits 写法香不香?评论区聊聊!老粉优先回复 👇

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。