Vue props 子组件传值总是慢一步?props 中的值没有及时更新的原因

270 阅读4分钟

前言

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 = 0trigger = 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 的响应式原理有关,其实核心原因是组件的更新是异步的。而这不关视图的渲染。当在父组件中更新数据时,尽管响应式数据立即更新,但这些更新触发的组件渲染是异步的。

  1. 父组件的数据已更新,但父组件的重新渲染尚未发生
  2. 调用子组件方法时,子组件仍在使用旧的 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 的响应式系统设计决定的,它批量处理更新以提高性能,避免不必要的重复渲染。