前言
Vue 的核心就在于其响应式原理实现,在实际项目中,我们经常会遇到与之相关的问题,比如「数据失去响应式」「数据更新了,视图却没更新」「props 拿不到最新值」等等。同时,异步是 JavaScript 的核心概念之一,是很多疑难杂症的根本原因。 本文通过一个简单的 props 传值示例,带你还原「子组件 props 总是滞后一步」的本质,并给出几种常见的解决思路。
问题
父组件:
<script setup>
import { ref } from "vue";
import Modal from "./components/Modal.vue";
const box = ref(null);
const modal = ref(null);
const curRadius = ref(0);
const triggerEle = ref(null);
function openModal(target, radius) {
triggerEle.value = target;
console.log("triggerEle.value: ", triggerEle.value);
curRadius.value = radius;
console.log("curRadius.value: ", curRadius.value);
modal.value?.open();
}
</script>
<template>
<div>{{ curRadius }}</div>
<div>{{ triggerEle }}</div>
<div ref="box" class="box" @click="openModal(box, 200)"></div>
<!-- 子组件 -->
<Modal ref="modal" :trigger="triggerEle" :radius="curRadius" />
</template>
<style scoped>
.box {
width: 100px;
height: 100px;
background-color: #42b883;
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
子组件:
<script setup>
import { ref } from "vue";
const props = defineProps({
radius: {
type: Number,
default: 100,
},
trigger: {
type: Object,
default: null,
},
});
function open() {
const { radius, trigger } = props;
console.log("props: ", props);
}
defineExpose({
open,
});
</script>
<template>
<div>{{ radius }}</div>
<div>{{ trigger }}</div>
</template>
demo 代码很简单,父子组件通过 props 进行普通的父子传值,只是父组件的 openModal
方法先更新数据(子组件需要的 props),然后调用子组件的方法。子组件在 open
方法中使用 props。
如果我们这样做,就会发现,当第一次点击 box 的时候,子组件获取的 props 为初始值,第二次点击时才能获取赋值。也就是子组件获取 props 的值总是晚一步,获取的是上一次的值。
- 第一次点击:子组件
open()
拿到的仍然是radius = 0
、trigger = null
; - 第二次点击,才是上一次点击的值。
我们在 openModal
方法内对 triggerEle 和 curRadius 打印,发现会立即赋值,这是没问题的。而且页面中也进行了正确的渲染:
<!-- 父组件 -->
<div>{{ curRadius }}</div>
<div>{{ triggerEle }}</div>
<!-- 父组件 -->
<div>{{ radius }}</div>
<div>{{ trigger }}</div>
页面上:
但是为什么传递给子组件的 open
方法,props 就不能及时更新呢?
网上有很多关于“ Vue props 异步数据传递”的文章,主要是父组件更新 props 时,数据是异步获取的。
有一些文章,也介绍了本篇同样的问题,总结为 props 为异步传值。
修复
我们先看看如何修复,关于 Vue 的异步更新问题,大多能通过 nextTick 解决:
nextTick(() => {
modal.value?.open();
});
另外一种解决我们问题的方式是,不需要 props 传值,直接向子组件方法中传参:
modal.value?.open(target, radius);
其他诸如在子组件中 watch 监听 props 的变化,就不多介绍了。
原因
那么,这个现象的原因是什么呢?
如果说 props 的传递是异步的,那么这在源码中是怎么体现的?为什么要这么设计呢?而且为什么我们好像很少受到这个异步的影响?
我们知道 Vue 的响应式原理,知道数据的更新并不会立即导致视图的更新,视图的更新是异步的。但是这里并没有视图的更新,只是进行 props 值传递而已啊。
我们很清楚这跟 Vue 的响应式原理有关,其实核心原因是组件的更新是异步的。而这不关视图的渲染。当在父组件中更新数据时,尽管响应式数据立即更新,但这些更新触发的组件渲染是异步的。
- 父组件的数据已更新,但父组件的重新渲染尚未发生
- 调用子组件方法时,子组件仍在使用旧的 props,因为它还没有接收到更新
所以问题就出在,当我们用在父组件中调用子组件方法使用子组件的 props 时,子组件方法是同步执行的,立即使用 props,此时 props 还未得到更新。也即,在父组件更新 props 传值时,子组件并不能立即拿到最新的 props:
triggerEle.value = target;
<Modal ref="modal" :trigger="triggerEle" :radius="curRadius" />
更新 triggerEle,并不像普通 JavaScript 变量赋值一样,Modal 中的 props 立即更新。
我们想要在父组件更新数据的同时,立即使用子组件的 props,但此时子组件的 props 是旧的。
因此,不是 props 的传递是异步的,而是组件的更新是异步的。当你修改父组件的数据后,直到下一个微任务周期,这些更新才会反映到子组件的 props 中。
这是 Vue 的响应式系统设计决定的,它批量处理更新以提高性能,避免不必要的重复渲染。