问题描述
因为业务需要在A页面中的一个按钮上绑定一个点击事件,点击时打开一个弹窗,同时给该弹窗传递一些数据。听起来似乎很简单,但其中有一些小问题值得记录一下,下面用一个小demo来模拟一下。
--父组件App.tsx--
setup() {
const defaultV = ref<Object>({});
const visible = ref<boolean>(false);
const handleClick = () => {
visible.value = true;
defaultV.value = {
id: 0,
content: '睡觉',
};
};
return () => (
<div>
<el-button type="primary" onClick={handleClick}>
Click me!
</el-button>
<DialogDemo defaultV={defaultV.value} v-model={visible.value} />
</div>
);
},
父组件中的代码很简单,渲染了一个按钮和一个弹窗子组件DialogDemo。给DialogDemo传入两个props:默认值对象defaultV和控制弹窗显隐的visible。当点击按钮时打开弹窗并给defaultV赋默认值。
--子组件DialogDemo.tsx--
setup(props) {
const visible = useVModel(props, "modelValue");
const selectedWork = ref(props.defaultV.id);
const workContent = ref<Object[]>([props.defaultV]);
onMounted(() => {
getWorkContent();
});
const getWorkContent = () => {
setTimeout(() => {
const resData = [
{
id: 1,
content: "上班",
},
{
id: 2,
content: "摸鱼",
},
{
id: 3,
content: "吃饭",
},
];
workContent.value = [...workContent.value, ...resData];
}, 1000);
};
return () => (
<el-dialog v-model={visible.value} title="弹窗demo">
<el-form>
<el-form-item label="工作选择">
<el-select v-model={selectedWork.value}>
{workContent.value?.map((item: any) => (
<el-option label={item.content} value={item.id} key={item.id} />
))}
</el-select>
</el-form-item>
</el-form>
</el-dialog>
);
},
弹窗子组件的显隐控制使用了VueUse工具库中的useVModel()来实现visible的双向绑定,现在Vue3.4+也可以使用defineModel()来实现。在该弹窗中渲染了一个下拉选择框表单,其默认选择项selectedWork为父组件props中传递的defaultV的id,其余选择项通过getWorkContent函数模拟接口请求回来。
这里的selectedWork如果用ref定义则不能直接赋值为props.defaultV,再给el-select的v-model绑定selectedWork.value.id,否则选择项一直是传入的默认值,用reactive定义则可以,难道它内部只能绑定ref和reactive定义的响应式变量?
一切看起来似乎没有问题,担当我们打开弹窗后发现:
默认选择项为空,同时选择项第一项为空。
问题分析
经过一阵分析之后,发现问题出在父子组件的渲染顺序上。Vue3中父子组件渲染的生命周期是父beforeMount => 子beforeMount => 子mounted => 父mounted ,同时setup还在beforeMount之前。在父子组件中的生命周期函数中添加打印可以验证这一点:
子组件中给selectedWork和workContent赋值的操作是在setup中的,赋值时父组件的点击事件还未触发,props中父组件的值defaultV还未传递过来,所以页面中渲染的默认选择项和选择项第一项都为空(弹窗正常打开是因为visible是用useVModel双向绑定的,父组件任何时候修改visible的值都会同步给子组件)。
问题解决
分析出了问题发生的原因,就要想办法让子组件获取到props后再进行赋值,这里想到了两种方法
1.监听父组件的点击事件
既然父组件是在点击事件之后再给defaultV赋值的,那就想办法监听到父组件点击事件后子组件再取props的值,而在子组件刚好存在一个可以知道父组件是否点击的变量visible,所以我们可以选择在子组件中监听visible的值,当visible为true时在进行赋值,因为只是赋初始值,所以使用一次性监听器(Vue3.4+),否则workContent会一直增加:
--子组件DialogDemo.tsx--
const selectedWork = ref('');
const workContent = ref<Object[]>([]);
watch(
visible,
(val) => {
if (val) {
selectedWork.value = props.defaultV;
workContent.value.unshift(props.defaultV);
}
},
{ once: true }
);
注意将workContent初始值赋为空,否则会多出一个空白选择项。此时再打开弹窗,默认选择传入的defaultV,其余项也可以正常选择:
2.改变子组件的渲染逻辑
如果能想办法让子组件在父组件完全挂载点击按钮后再渲染,这样子组件就能在setup中就拿到props。这种方法只需在父组件中加一行判断,当visible值为true时再渲染子组件,子组件代码不需要改变:
--父组件App.tsx--
{visible.value && (<DialogDemo defaultV={defaultV.value} v-model={visible.value} />)}
这样其实更符合直觉,且更节约性能。试想若父组件是一个很多行的表格,表格中的每行都有一个按钮,每个按钮都对应一个弹窗。如果在父组件渲染时就将所有的弹窗子组件挂载,这样会造成很大的性能开销,且用户可能只会用到其中几个弹窗。
页面闪烁问题
如果默认选择项是接口返回的数据或者子组件的下拉框中的选择项过多时,改变子组件的渲染逻辑就有可能出现页面闪烁问题(即默认选择项先显示value再显示label或点击下拉框时开始无选择项再突然出现选择项)。这都是因为子组件在点击按钮后在渲染请求数据,可能会“来不及”。此时就需要使用await等接口返回数据之后再给选择框数据赋值:
--子组件DialogDemo.tsx--
await getWorkContent(); // await不能等待setTimeout,这里只是模拟接口请求场景
selectedWork.value = defaultV.id;
workContent.value.unshift(defaultV);