第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\n
\n\n
\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 把 props 和 emit 封装成了一行,父组件通过 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 各有优势,选工具要看场景
延伸学习资源:
- Vue 3 官方文档 - defineModel (英文原版,最准确)
- Vue 3 Vapor Mode RFC (英文,社区讨论)
- Solid.js 官方文档 (想了解 Solid 对比可以看)
互动钩子:你在项目里用过 defineModel 吗?感觉它比传统的 props + emits 写法香不香?评论区聊聊!老粉优先回复 👇

评论(0)