
JavaScript 的可选链与空值合并
?. 和 ?? 是 JavaScript 的运算符,不是 Vue 的 API。模板里读嵌套数据几乎离不开它们。
安全读取嵌套属性
读链式属性时,链上任意一环是 null 或 undefined,整个表达式就短路返回 undefined,不再往后读,也不会抛错。
嵌套数据长这样:
const user = { info: { address: { city: '北京' } }}不加 ?.,链上某一环没值就直接报错,后面的代码不再执行:
// TypeError: Cannot read properties of undefinedconst city = user.info.address.city补上之后可以安全读到最后一层:
const city = user?.info?.address?.city规则只有两条:前面是 null 或 undefined 就停止读取,返回 undefined;前面有值就继续往后取。
四种写法:
obj?.a?.b?.c // 读深层属性arr?.[0] // 读数组元素fn?.() // 调用方法前先判断obj?.[key] // 读动态 key递归组件里判断子节点是常见用法:
<TreeItem v-if="item?.children?.length" />item 不存在时表达式返回 undefined,v-if 为假,不渲染;item 存在但 children 是空数组时 length 为 0,同样不渲染。
这段判断等价于 if (item && item.children && item.children.length)。
默认值兜底
|| 会把 0、空字符串和 false 一起当成空值:
const num = 0
console.log(num || 100) // 100,0 被当成空值console.log(num ?? 100) // 0,只判断 null 和 undefined?? 只把 null 和 undefined 当成空值,其他值原样返回,0、空字符串和 false 都是有效值。
组合使用
两者一起用是最常见的兜底写法:能安全读到值就用它,读到 undefined 就补默认值。
const city = user?.info?.address?.city ?? '未知城市'const name = data?.user?.name ?? '匿名用户'组件里给 prop 补默认值用的是 withDefaults,它只处理 prop 没有传的情况:
const props = withDefaults(defineProps<{ count?: number}>(), { count: 0})动态组件
动态组件让多个组件共用同一个挂载点,由数据决定当前渲染哪一个,靠内置的 <component> 标签和 is 属性实现。官方文档见 动态组件。
基本写法
<script setup>import { shallowRef } from 'vue'import A from './A.vue'import B from './B.vue'
const currentComp = shallowRef(A)</script>
<template> <component :is="currentComp" /></template><component>是内置标签,本身不渲染内容,渲染谁由is决定。is的值可以是组件对象、注册过的组件名字符串,也可以是一个 ref,模板里会自动解包。- 换掉
currentComp.value,挂载点里的组件跟着换。
这里用 shallowRef 而不是 ref,原因见“组件不要放进响应式代理”一节。
典型场景
- Tab 切换
- 步骤表单
- 低代码平台和动态仪表盘
- 权限控制下的动态视图
组件名与组件对象
is 接受两种值:组件对象,或者字符串。字符串既可以是注册过的组件名,也可以是原生 HTML 标签名,例如 :is="'div'"。Vue 2 和 Vue 3 在这点上一致。
在 <script setup> 里通常传组件对象,原因是 import 进来的组件只是当前作用域里的变量,没有注册到任何名字下,用名字找不到它。
// 选项式 API:组件注册过名字,可以传名字export default { components: { A, B }, data() { return { compName: 'A' } },}<!-- script setup:没有注册名,直接传 import 进来的对象 --><component :is="A" /><table> <tr is="vue:my-row"></tr></table>组件不要放进响应式代理
组件定义是静态对象,不需要响应式。把它交给 reactive 或 ref,Vue 会多做一层深层代理,开发环境还会打印一条警告:
这条消息来自 packages/runtime-core/src/vnode.ts 里的 createVNode。开发构建里,Vue 会顺带调用 toRaw(type) 把它还原成原始对象;生产构建没有这一步,组件仍然带着这层代理,多出来的代理就是额外的开销。
markRaw
markRaw 把一个对象标记成永远不转为响应式,并返回这个对象本身。放在集合里的组件都用它包一层:
import { markRaw, reactive } from 'vue'import A from './A.vue'import B from './B.vue'
const tabList = reactive([ { name: 'A 组件', comp: markRaw(A) }, { name: 'B 组件', comp: markRaw(B) },])标记跟着对象本身走,之后 reactive 遇到它就直接原样返回。
shallowRef
shallowRef 只监听 .value 有没有被替换,不把内部对象转成代理,适合单独存一个当前组件:
import { shallowRef } from 'vue'import A from './A.vue'import B from './B.vue'
const currentComp = shallowRef(A)
currentComp.value = B // 触发更新,挂载点换成 B要放进集合的组件用 markRaw,只存单个引用时用 shallowRef。
完整示例
三个文件构成一个可以运行的例子:两个标签页子组件,一个父组件负责切换、传参和缓存。
src/├── components/│ ├── TabA.vue│ └── TabB.vue└── App.vueTabA 有一个输入框和一个按钮,按钮向上抛出事件:
<script setup lang="ts">import { ref } from 'vue'
defineProps<{ msg: string}>()
const emit = defineEmits<{ (e: 'send', val: string): void}>()
const inputVal = ref('')
const emitData = () => { emit('send', '我是 A 组件的数据')}</script>
<template> <div class="box"> <h3>组件 A</h3> <p>父组件传值:{{ msg }}</p> <input v-model="inputVal" placeholder="缓存测试" /> <button @click="emitData">向父传值</button> </div></template>
<style scoped>.box { padding: 20px; border: 1px solid #ccc; }</style>TabB 只有一个 textarea,用来验证切换后输入的内容还在:
<script setup lang="ts">import { ref } from 'vue'
defineProps<{ msg: string}>()
const text = ref('')</script>
<template> <div class="box"> <h3>组件 B</h3> <p>父组件传值:{{ msg }}</p> <textarea v-model="text" placeholder="切换不丢失内容"></textarea> </div></template>
<style scoped>.box { padding: 20px; border: 1px solid #ccc; margin-top: 10px; }</style>父组件用 markRaw 存列表、用 shallowRef 存当前组件,<KeepAlive> 包住挂载点:
<script setup lang="ts">import { markRaw, reactive, shallowRef } from 'vue'import TabA from './components/TabA.vue'import TabB from './components/TabB.vue'
// 组件放在列表里,用 markRaw 挡住 reactive 的深层代理const tabList = reactive([ { name: 'A 页面', comp: markRaw(TabA) }, { name: 'B 页面', comp: markRaw(TabB) },])
// 只存当前组件,用 shallowRef 监听 .value 的替换const currentComp = shallowRef(TabA)
const handleReceive = (val: string) => { console.log('父组件收到:', val)}</script>
<template> <button v-for="item in tabList" :key="item.name" :class="{ active: currentComp === item.comp }" @click="currentComp = item.comp" > {{ item.name }} </button>
<KeepAlive> <component :is="currentComp" msg="父组件下发的文本" @send="handleReceive" /> </KeepAlive></template>
<style>button.active { background: #409eff; color: #fff; border: none; padding: 4px 8px; margin: 0 4px; }</style>用 KeepAlive 缓存状态
动态组件切换时默认销毁旧组件,再切回来会重新创建。用 <KeepAlive> 包住挂载点,Vue 会缓存组件实例:
<KeepAlive> <component :is="currentComp" /></KeepAlive>缓存下来的是组件实例和它内部的状态,输入框里已经输入的内容和组件里的 ref 都会留着,切回来直接复用。
组件被缓存和重新激活时会触发 onActivated 和 onDeactivated。include 和 exclude 按组件名决定哪些组件进入缓存,max 限制缓存数量,超出上限时淘汰最久没有访问的实例。
官方文档见 KeepAlive。
Vue 3 学习笔记
连载中分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
最后更新于 ,距今已过 208 天
部分内容可能已过时