vue3中一个常见场景,父组件向后端异步获取数据,再父子传参给子组件,由子组件来渲染获取到的数据。
简单的props父子传参
只用props进行父子传参,子组件在其一系列生命周期开始时是获取不到数据的。父子组件的生命周期流程如下:
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
生命周期钩子是同步执行的,而向后端获取数据是异步的,如果在父组件的created中向后端异步获取数据,根据事件循环的顺序,子组件开始创建时是获取不到数据的。
做一个实现进行验证
//父组件,getArticleById向后端获取数据
const initArticleForm = () => {
console.log('父组件created')
getArticleById(articleId).then(res => {
console.log('父组件获取到数据')
})
}
initArticleForm()
onMounted(()=>{
console.log('父组件Mounted')
})
可以看到放在父created中的异步获取要到父mounted后才能拿到数据。这样子组件是无法正常渲染的。
解决方案
1、使用v-if控制子组件开始挂载的时机
当异步获取到数据后再用v-if设置为true挂载子组件
<RichTextEditor v-if="flag" v-model="addArticleForm.content" ></RichTextEditor>
const initArticleForm = () => {
console.log('父组件created')
getArticleById(props.articleId).then(res => {
flag.value = true //获取到数据改变flag
let endTime = window.performance.now()
console.log('父组件获取到数据')
})
}
initArticleForm()
此时可以看到顺序是正确的,所以可以正常显示
性能测试
为了模拟卡顿的环境,将后端接口延迟3s返回
getArticleContentById: async (req, res) => {
const {id} = req.query
const data = await ArticleService.getArticleContentById(id)
await setTimeout(()=>{
res.header("Cache-Control", "no-cache, no-store, must-revalidate");
res.send({
code: 200,
msg: "获取成功",
data
})
}, 3000)
},
然后在组件中v-for出1000个相同div用来展示父组件传来的数据
<div v-for="item in list">{{props.modelValue}}</div>
<script setup>
let list = []
for(let i = 0; i < 10000; i++){
list.push(i)
}
</script>
子组件白屏时间计算:请求数据所需要的时间+子组件渲染的时间
共3104+109ms
2、子组件中用watch更新
用watch监视props变化,并通知试图对应的数据变化
let startTime = window.performance.now()
console.log('子组件created')
onMounted(()=>{
console.log(`子组件Mounted`)
})
const props = defineProps({
modelValue: {
type: String,
default: ""
}
})
watch(()=>props.modelValue, (newValue, oldValue) => {
valueEditor.value = props.modelValue//更新数据
console.log('watch到新数据并更新页面')
let endTime = window.performance.now()
console.log(`子组件加载白屏时间:${endTime-startTime}`)
})
此时子组件白屏时间的计算方式不再是请求数据时间+子组件渲染时间,因为子组件会在数据请求到达之前渲染完毕,在数据到达之后再更新对应dom节点,此时白屏时间为111ms,等待视图更新成获取到的数据,时间总共为3132ms。
3、子组件中用computed更新
let valueEditor = computed(()=>{
let endTime = window.performance.now()
console.log(`子组件最终更新时间:${endTime-startTime}`)
return props.modelValue
})
原理同watch基本一致,加载时间为58ms,最终更新时间为3114ms
可以看到使用computed的加载时间是要比watch快的,因为为了测试,在组件中v-for出1000个相同div用来展示父组件传来的数据,用computed有缓存这1000个div用的都是同一个缓存所以比较快。
骨架屏优化
使用suspense内置组件,要将原来的父组件放在suspense的插槽中,当数据没有加载到位时显示#fallback中的组件
<template>
<Suspense>
<template #default>
<parent></parent>
</template>
<template #fallback>
<div>loading...这里放骨架屏的组件</div>
</template>
</Suspense>
</template>
<script setup>
import {defineAsyncComponent} from "vue";
const parent= defineAsyncComponent(()=>import('./updateItem.vue'))
</script>
parent组件
<template>
<el-form :model="addArticleForm" label-width="120px" ref="addArticleFormRef" :rules="addArticleFormRule">
<el-form-item label="文章标题" class="form-item" prop="title">
<el-input class="upload-input" v-model="addArticleForm.title" placeholder="请输入文章标题"/>
<el-button type="primary" class="upload-btn" @click="upload(addArticleFormRef)">
<span v-if="!loading">修改文章</span>
<span v-else>上 传 中...</span>
<el-icon class="el-icon--right"><Upload /></el-icon>
</el-button>
</el-form-item>
<el-form-item label="文章分类" class="form-item" prop="tag">
<el-select v-model="addArticleForm.tag" placeholder="请选择文章分类">
<el-option v-for="key in user().articleTag.keys()" :label="key" :value="user().articleTag.get(key)"></el-option>
</el-select>
</el-form-item>
<el-form-item label="文章内容" class="form-item">
<RichTextEditor v-model="addArticleForm.content" v-model:textValue="addArticleForm.abstract"></RichTextEditor>
</el-form-item>
</el-form>
</template>
<script setup>
import RichTextEditor from "../../../components/RichTextEditor.vue";
import {ref, reactive} from "vue";
import {user} from "../../../store/index.js";
import {addArticle} from "../../../api/system.js";
import {getArticleById} from '../../../api/article.js'
import {useRouter,useRoute} from "vue-router";
const route = useRoute()
const addArticleForm = reactive({
tag: null, //Number类型
title: '',
content: null,
})
//请求数据,setup语法糖中使用顶层await,setup会自动在模块顶层加async,用await控制suspense组件中替换骨架屏的时机
let {data} = await getArticleById(route.params.articleId)//向后端请求
addArticleForm.tag = data[0].tag
addArticleForm.title = data[0].title
addArticleForm.content = data[0].content
</script>
此时父组件的子组件不再需要v-if ,watch,computed,可以直接用props中的数据