适用读者:写过 Vue 但被"改了数据视图不更新"折磨过的人;要在两套 API 间做技术选择的人。
前置:S14-J06、B03(响应式入门)、J01(VueRouter)、B05(工程化)。
覆盖旧文:4400(Vue2 基础)、4401(Vue3)、4402(Vuex)、4403(工程化开发)、4406(路由)、4405(尚品汇)、4511/4513/4514(小兔鲜儿)、4512(CSS 高级,项目用)→ 11 篇 / 约 20 万字符。
扩展点:E-S14-023(三种 API 形态的实质差异:this、复用、类型)、E-S14-024(ref/reactive的五个响应式边界)、E-S14-025(路由守卫执行顺序与权限落点)。
下一篇:S14-A03 · AI 时代 UI 生成方法论。
1. 一句话结论
Vue 的响应式只有一条规则:"渲染时读到的属性"才会被追踪,"没读到的"改了也不更新。所有"数据变了视图不动"的故障,都是这条规则的推论——reactive 解构、新增属性、数组下标、Object.freeze、跨组件传引用。
出处:
A-4400/A-4401(两套 API)、A-4402(Vuex)、A-4403(工程化)、A-4406(路由)。
2. 三种 API 形态的实质差异(E-S14-023)
| 维度 | Options API(Vue 2 起) | Composition API(Vue 3) | <script setup>(语法糖) |
|---|---|---|---|
| 组织方式 | 按选项分组(data/methods/computed) | 按逻辑分组(一个函数一件事) | 同 Composition,但自动暴露 |
this |
有,指向组件实例 | 没有 | 没有 |
| 逻辑复用 | mixin(来源不明、命名冲突) | 组合式函数(useXxx) |
同 |
| 类型推导 | 弱(需 Vue.extend/装饰器) | 好(TS 友好) | 最好 |
| 适用 | 老项目维护 | 新项目、复杂逻辑 | 新项目默认 |
<!-- Vue 2 / Options -->
<script>
export default {
data() { return { count: 0, list: [] } },
computed: { double() { return this.count * 2 } },
methods: { inc() { this.count++ } }
}
</script>
<!-- Vue 3 / script setup -->
<script setup lang="ts">
import { ref, computed } from 'vue'
const count = ref(0)
const list = ref<string[]>([])
const double = computed(() => count.value * 2)
const inc = () => { count.value++ }
</script>
判据:"逻辑复用"才是 Composition 的真正卖点——同一个功能(分页、请求、表单校验)能收成一个函数并用参数化差异,而不是散落在 data/methods/watch 三处。
3. 五个响应式边界(E-S14-024)
// ① reactive 解构会丢响应性
const state = reactive({ n: 0 })
const { n } = state // ❌ n 是普通数字快照
const n = toRef(state, 'n') // ✅ 或直接用 state.n
// ② reactive 对原始类型无效
const s = reactive(0) // ❌ 不报错但无响应
const r = ref(0) // ✅
// ③ 整体替换 reactive 对象会断链
let state = reactive({ a: 1 })
state = reactive({ a: 2 }) // ❌ 模板还指着旧代理
Object.assign(state, { a: 2 }) // ✅
// ④ 数组按索引赋值在 Vue2 中不触发(Vue3 Proxy 已修复)
// Vue2: vm.$set(list, 0, x) / Vue3: list[0] = x 即可
// ⑤ Object.freeze / 非响应式数据源
const raw = Object.freeze({ a: 1 }) // 不会建立响应
| 边界 | 症状 | 修法 |
|---|---|---|
| 解构丢响应 | 改 state.n 视图不动 |
toRefs/toRef 或不解构 |
reactive(原始值) |
静默失效 | 用 ref |
替换整个 reactive |
视图停留在旧值 | Object.assign 或 ref 包对象 |
| Vue2 索引/新增属性 | 视图不更新 | $set / $delete(Vue3 无需) |
ref 忘记 .value |
模板里自动解包,JS 里必须 .value |
统一约定 |
深层响应式:ref(obj) 会对对象内部做深层代理;若只要浅层,用 shallowRef(大对象/第三方实例的性能优化)。
4. 状态管理:Vuex 与 Pinia(4402 的比对)
| 维度 | Vuex 4 | Pinia |
|---|---|---|
| 变更方式 | 必须 commit('mutation'),同步 |
直接改 store.count++ |
| 异步 | dispatch('action') |
action 就是普通函数 |
| 模块 | modules + namespaced |
每个 store 独立定义 |
| TS 支持 | 需额外类型体操 | 原生友好 |
| devtools | 支持 | 支持(时间旅行) |
| 适用 | 老项目 | Vue 3 新项目默认 |
// Pinia
export const useCart = defineStore('cart', {
state: () => ({ items: [] }),
getters: { count: (s) => s.items.length },
actions: { add(item) { this.items.push(item) } }
})
判据:状态该不该进 store——跨组件共享 + 需要持久/可回溯才进;组件内状态留在组件里(避免 store 变成"全局变量袋")。
5. 路由:history vs hash、守卫顺序、权限落点(E-S14-025)
| 模式 | URL | 需要服务端配合 | 适用 |
|---|---|---|---|
createWebHistory |
/user/1 |
需要 fallback(try_files … /index.html) |
生产推荐(见 J06 §3) |
createWebHashHistory |
/#/user/1 |
不需要 | 静态托管、内嵌页面 |
守卫执行顺序(必背):
组件内 beforeRouteLeave → 全局 beforeEach → 路由独享 beforeEnter
→ 组件内 beforeRouteEnter → 全局 beforeResolve → 全局 afterEach
router.beforeEach(async (to) => {
if (!to.meta.requiresAuth) return true
const ok = await checkLogin()
if (!ok) return { name: 'login', query: { redirect: to.fullPath } } // 未登录回登录页
})
| 权限落点 | 做法 | 边界 |
|---|---|---|
| 路由级 | meta.requiresAuth + 守卫 |
前端守卫不是安全边界(接口必须自己鉴权) |
| 菜单级 | 按权限过滤路由表 | 动态 addRoute 后要 router.replace 一次 |
| 按钮级 | 指令/组件封装 v-perm |
仅 UI 层,接口仍需校验 |
判据:前端权限只解决"看不看得见",接口鉴权才解决"能不能做"——把两者混为一谈是常见漏洞来源。
6. 三套项目结构的共性(尚品汇 / 小兔鲜儿 / 学成在线)
src/
├─ api/ 按域拆模块,统一 request 封装(拦截器处理 token/错误码)
├─ assets/ 图片、字体(可被构建处理)
├─ components/ 通用组件(无业务依赖)
├─ composables/ useXxx(分页、请求、校验)
├─ layouts/ 布局壳(头部/侧栏/内容区)
├─ router/ 路由表 + 守卫
├─ stores/ Pinia 模块
├─ styles/ 变量、reset、工具类
├─ utils/ 纯函数
└─ views/ 页面(对应路由)
| 共性 | 说明 |
|---|---|
| 页面 = 路由单元 | views 与 router 一一对应,组件不放业务请求 |
| 请求集中 | api/ + 统一实例,避免每页各自 fetch |
| 通用组件与业务组件分离 | 业务组件放页面目录下,避免 components 膨胀 |
| 样式三件套 | reset + 变量(design token)+ 工具类 |
7. 反例:六种"经典不更新/不生效"
| 写法 | 问题 |
|---|---|
const { list } = reactive({ list: [] }) |
解构丢响应 |
ref 在 JS 里当数字用(忘 .value) |
改的是先前的值 |
watch 监听 reactive 对象整体 |
需 deep: true 或监听具体字段 |
守卫里 next() 与 return 混用 |
双次执行/死循环 |
| 用 hash 模式却配了 history 的 fallback | 无影响但配置误导 |
| 把权限判断只写在前端守卫 | 接口可被直接调用 |
8. 自测
- Options 与 Composition 的实质差异(三条)?
ref与reactive的边界?reactive为什么不能装原始值?- 解构丢响应怎么修?
- 路由守卫的完整顺序?前端权限的边界?
- Vuex 与 Pinia 的关键差别?
答案
this有无、复用机制(mixin vs composable)、类型推导。ref可装任意值需.value;reactive只能对象/数组,因代理需 target 为对象。toRef/toRefs或不解构。- leave → beforeEach → beforeEnter → beforeRouteEnter → beforeResolve → afterEach;前端只管可见性。
- Vuex 强制 mutation、模块需 namespaced;Pinia 直接改 state、原生 TS。
9. 下一篇
S14-A03 · AI 时代 UI 生成方法论 —— 从形容词到规格书:视觉层级、留白与对齐、颜色与字体的预算、状态三件套、习惯用法与"AI 味"的来源。
本篇属于系列《S14 · 前端与可视化》。旧文来源:A-4400–4406、A-4405、A-4511–4514;扩展点 E-S14-023/024/025。库版本按原文写作时点,落地前需核对官方 release(标待核)。