阅读视图

发现新文章,点击刷新页面。

Dialog组件状态建模规则

本文所说的组件状态建模规则,特别适用于:Dialog 生命周期长、渲染早于数据的组件

核心设计目标

UI 状态建模(template)的第一目标不是语义最精确,而是结构稳定、可渲染、可推导

简单说,template绑定的变量初始值不能为undefined或者null,最好是预定义的空模板。

二、基础概念划分(这是地基)

区分三种“状态层级”

层级 例子 规则
UI 结构状态 表单字段、列表项、dialog 内容 必须结构稳定
UI 行为状态 visible / loading / disabled 可 boolean / enum
业务数据状态 接口返回对象 可 null / undefined

template建模只会和UI结构状态和行为状态有关,和业务数据状态无关。

三、最重要的规则(90% 的坑在这里)

规则 1:**template 绑定的数据,禁止null,推荐属性确定的空数据结构

不推荐

const element = ref(null)
{{ element.id }}

推荐

const element = ref({
  id: '',
  name: '',
})

理由不是“防报错”这么简单,而是:

render / computed / watch(immediate)
会在“业务数据尚未准备好”之前运行

规则 2:null 表示“概念不存在”,而 UI 中很少真的“不存在”

状态 推荐建模
UI状态尚未准备好 空的属性确定的数据结构
业务对象不存在 null
接口失败 error state

四、关于 computed / watch 的建模规则

规则 3:template绑定的computed = 一开始就要有稳定的数据结构

computed从undefined或者null变化为{id:'xxx'},这就称作不稳定

// 不稳定
const id = computed(() => props.element.id)

稳定方案一(首选)

props.element = { id: '' }

稳定方案二(兜底)

const id = computed(() => props.element?.id ?? '')

方案二是 防御,不是建模优雅

规则 4:watch(immediate) 必须当作“setup 同步代码”对待

watch(
  () => props.element,
  (el) => {
    // 这里 ≈ setup 中直接访问
  },
  { immediate: true }
)

所以规则是:

凡是会被 watch(immediate) 读取的数据
都必须在 setup 结束前是安全的

安全的意思是watch的回调函数中需要用guard子句排除到props.element是undefined或者null这种情况。不然会报错。

规则 5:composable 永远假设“调用方是不可靠的”

useSomething(element)

composable 内部必须:

  • guard 参数
  • 不假设结构存在
  • 不信任生命周期顺序
if (!element || !element.id) return

这是 composable 的防御职责 如果你在组件内部写满 if (!xxx) return
那说明状态模型有问题

规则 6:弹框类组件 = 提前存在,延后可见

visible = false // 控制显示
element = {id:"", ...}    // 内容占位

不要用 visible = false 的同时element=ref(null)

这里又一次说明null和空数据结构的区别:null表示不存在,空数据结构表示存在,但内容未准备好。不存在的就不能正常渲染,空的数据结构是可以正常渲染的。

相关知识

vue组件首次渲染执行任务顺序 vue列表渲染设计决策

vue首次渲染完整时间线

错误的预判: watch(immediate)只有在openDialog事件执行之后,v-model变为true的时候才会执行回调。

一、watch(immediate)的执行时机

阶段 发生的事
setup 创建响应式、watch、computed
watch(immediate) 就在 setup 过程中执行
render 读取 template 表达式
beforeMount 准备挂载
mounted DOM 已经插入

👉 watch(immediate) 比 render 还早

所以:

  • 不受 v-if 影响
  • 也不等 DOM

下面的行为都不受组件内部的v-if的影响

行为 执行阶段
watch(immediate) setup
模板中 {{ xxx }} 被访问 render 阶段
v-for source 被读取 render 阶段

二、组件内部的v-if和组件外部的v-if的本质区别

情况 A:v-if 在组件外部(控制组件是否存在)

<Child v-if="show" />

show === false 时:

项目 是否发生
创建组件实例
执行 setup
注册 watch
render template
读取任何表达式

👉 Child 在运行期完全不存在

情况 B:v-if 在组件内部(控制局部渲染)

<template>
  <div v-if="show">
    {{ foo }}
  </div>
</template>

show === false 时:

项目 是否发生
创建组件实例
执行 setup
watch(immediate)
render() 调用
读取 foo ❌(因为分支不走)

👉 组件存在,但该分支在 render 中被跳过

结论

  • 外部的v-if,整个组件的实例都不存在,setup阶段都不会执行
  • 内部的v-if, 只有v-if的部分的render不会执行,其余的操作都会执行。

vue首次渲染

Vue 3 组件首次渲染完整时间线

顺序 官方钩子 / 阶段 发生的事
1 setup 初始化状态、注册 watch / computed
2 watch(immediate) 在 setup 执行期间立即触发
3 render(内部) 执行 render 函数,读取 template 表达式
4 onBeforeMount DOM 挂载前
5 mount(内部过程) 创建并插入真实 DOM
6 onMounted DOM 已插入页面

👉 这里的「mount」是内部过程,不是钩子

Vue 列表渲染设计决策表(v-for source)

使用场景:

  • v-for
  • computed → v-for

一、v-for 数据源设计(最核心)

问题 必须满足 正确做法 错误做法 后果
v-for 的 source 初始值是什么? 必须可遍历 [] / {} / Map() undefined / null / false 列表语义无法建立
source 的类型是否稳定? 类型不可跃迁 [] → [...] undefined → [] diff 通道缺失
是否依赖 ?. 比如 v-for = listItem in object?.list object?.list不能是undefined 初始值:object=reactive({list:[]}) 初始值:object=ref(undefined) 列表语义无法建立

二、computed + v-for 决策

场景 推荐 不推荐 原因
computed 作为 v-for source 返回 [] 返回 undefined undefind会导致未建立列表语义结构
computed 内部判空 ?. ?? [] if (!x) return 避免短路,短路会导致v-for source为undefined
computed 首次执行 访问完整结构 return {list:[]}这样的预定义稳定结构 v-for source只会在首次建立列表语义结构,即使source值变化了,也不会再重新建立语义结构
v-for 绑定 computedList computed?.list v-for source不能依赖?.,因为可能返回undefined,会导致未定义列表结构语义

三、看到v-for检查设计列表

  • v-for 的 source 第一次 render 是不是数组 / 对象?

  • 是否存在 undefined → array 的路径?

  • 是否用了 ?. 直接喂给 v-for?

  • computed 是否可能 return 非遍历值?

四、可以反复使用的代码模版

const state = reactive({
  list: [],
  loading: true
})

onMounted(async () => {
  state.list = await fetchList()
  state.loading = false
})
<template>
  <div v-if="state.loading">loading...</div>
  <div v-else>
    <div v-for="item in state.list" :key="item.id" />
  </div>
</template>

五、关于v-for的统一心智模型

你可以把 Vue 渲染分成三层:

① 编译期(决定结构)
② 首次 render(建立语义)
③ diff 更新(只做比较)

v-for 的“可遍历语义”只在第 ② 步建立一次

如果你在第 ② 步给了:

  • undefined
  • null
  • false

👉 后面改不回来了,即使v-for source变了也不会重新建立

结果:列表一定始终渲染不出来

编译期报错的后果

如果在编译期v-for source = souceObject.property,而且source初始值为null,那么必然报错Can not read property of null (reading property),编译期报错会导致白屏

❌