小伙伴们在写Vue3项目时,是不是常遇到这种情况:组件之间传数据,数据明明改了,页面却纹丝不动?明明控制台打印数据是新的,界面就是不跟更,简直让人挠头!其实呀,这不是bug,大概率是咱们没摸透Vue3的“响应式套路”,今天就用唠嗑的方式把这事讲明白~
先搞懂:为啥数据变了页面不刷新?
Vue3靠“响应式系统”驱动页面更新,简单说就是:只有被Vue“盯紧”的数据,改了才会通知页面刷新。要是数据没被“盯”上,哪怕你改得再欢,页面也懒得理你。而组件通信时的不刷新,基本都绕不开这俩坑:要么是传的非响应式数据,要么是修改方式不对,把响应式给“玩坏”了。
常见场景+踩坑解决,手把手教你避坑
场景1:父传子,子组件拿不到最新值
父组件给子组件传数据,这是最基础的操作吧?但有时候父组件数据改了,子组件却还是老样子,比如这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 父组件 Parent.vue -->
<template>
<Child :user="user" />
<button @click="changeUser">改名字</button>
</template>
<script setup>
import Child from './Child.vue'
let user = { name: '张三' }
const changeUser = () => {
user = { name: '李四' }
}
</script>
这时候点按钮,父组件的user确实变了,但子组件就是不刷新。为啥?因为最开始的user虽然是响应式的,但咱们直接给user“换了个对象”,新对象没被Vue的响应式系统“注册”,子组件自然没收到通知。
✅ 解决办法:别直接换对象,改对象里的属性;或者用ref包裹对象(更稳妥)
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 父组件 正确写法 -->
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
let user = ref({ name: '张三' })
const changeUser = () => {
user.value.name = '李四'
}
</script>
场景2:子传父,父组件数据变了但页面没更
子组件通过emit给父组件传值,父组件接收后数据改了,页面却没反应。这种情况大概率是父组件“手动存值”时,把响应式弄丢了。比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<button @click="sendMsg">传消息给父组件</button>
</template>
<script setup>
const emit = defineEmits(['send-msg'])
const sendMsg = () => {
emit('send-msg', '我是子组件的消息')
}
</script>
<template>
<Child @send-msg="handleMsg" />
<p>{{ msg }}</p>
</template>
<script setup>
import Child from './Child.vue'
let msg = ''
const handleMsg = (newMsg) => {
msg = newMsg
}
</script>
一目了然吧?父组件的msg就是个普通变量,没有用ref或reactive包裹,改了自然不会触发刷新。
✅ 解决办法:给接收数据的变量加“响应式外套”,用ref包基本类型,用ref/reactive包复杂类型
1
2
3
4
5
6
7
8
9
10
<!-- 父组件 正确写法 -->
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
let msg = ref('')
const handleMsg = (newMsg) => {
msg.value = newMsg
}
</script>
场景3:兄弟组件通信,数据传了但页面不更
兄弟组件之间没直接关系,一般用“事件总线”或“Pinia状态管理”传数据。这里先讲简单的事件总线(Vue3里要自己封装哦),踩坑点还是“响应式”。
比如咱们封装个简单的事件总线:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ref } from 'vue'
const bus = ref(new Map())
export const useEventBus = () => {
const on = (name, callback) => {
bus.value.set(name, callback)
}
const emit = (name, ...args) => {
const callback = bus.value.get(name)
callback && callback(...args)
}
return { on, emit }
}
要是兄弟A传数据给兄弟B时,B组件接收后存到了非响应式变量里,照样不刷新。所以核心还是:接收数据的变量必须是响应式的!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 兄弟A组件 -->
<script setup>
import { useEventBus } from './eventBus.js'
const { emit } = useEventBus()
const sendToB = () => {
emit('brother-msg', '来自A的消息')
}
</script>
<!-- 兄弟B组件 正确写法 -->
<script setup>
import { ref } from 'vue'
import { useEventBus } from './eventBus.js'
const { on } = useEventBus()
let msg = ref('')
on('brother-msg', (newMsg) => {
msg.value = newMsg
})
</script>
场景4:Pinia状态变了,组件不刷新
用Pinia管理全局状态时,也可能遇到“状态改了页面不更”的情况,多半是这俩问题:
-
直接修改Pinia的原始数据,没走响应式:比如在Pinia里定义了个对象,组件里直接给对象赋值新的,没改属性也没用水合(hydrate)。
-
组件里“解构”状态时,把响应式弄没了:比如用const { count } = useStore(),这样count就变成普通变量了,改了Pinia里的状态,组件也收不到。
✅ 解决办法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { defineStore } from 'pinia'
export const useMyStore = defineStore('myStore', {
state: () => ({
user: { name: '张三' },
count: 0
}),
actions: {
changeName(newName) {
this.user.name = newName
},
addCount() {
this.count++
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 组件里正确使用 -->
<script setup>
import { useMyStore } from './store.js'
import { storeToRefs } from 'pinia'
const store = useMyStore()
console.log(store.count)
const { count, user } = storeToRefs(store)
const change = () => {
store.changeName('李四')
store.addCount()
}
</script>
最后总结个“万能避坑指南”
-
数据要“响应式”:基本类型(字符串、数字等)用ref,复杂类型(对象、数组)用ref或reactive,别用普通变量;
-
修改要“按规矩来”:ref包裹的改.value,reactive包裹的改属性(别直接换对象),Pinia状态用actions改或storeToRefs解构;
-
接收要“保响应”:不管是父传子、子传父还是兄弟通信,接收数据的变量必须是响应式的,别直接存到普通变量里。
其实Vue3的响应式没那么玄乎,核心就是让数据被Vue“盯牢”。下次再遇到不刷新的问题,先检查下数据是不是响应式的,修改方式对不对,基本都能解决~
<p>小伙伴们在写Vue3项目时,是不是常遇到这种情况:组件之间传数据,数据明明改了,页面却纹丝不动?明明控制台打印数据是新的,界面就是不跟更,简直让人挠头!其实呀,这不是bug,大概率是咱们没摸透Vue3的“响应式套路”,今天就用唠嗑的方式把这事讲明白~</p>
<h2><a id="_3"></a>先搞懂:为啥数据变了页面不刷新?</h2>
<p>Vue3靠“响应式系统”驱动页面更新,简单说就是:只有被Vue“盯紧”的数据,改了才会通知页面刷新。要是数据没被“盯”上,哪怕你改得再欢,页面也懒得理你。而组件通信时的不刷新,基本都绕不开这俩坑:要么是传的非响应式数据,要么是修改方式不对,把响应式给“玩坏”了。</p>
<h2><a id="_7"></a>常见场景+踩坑解决,手把手教你避坑</h2>
<h3><a id="1_9"></a>场景1:父传子,子组件拿不到最新值</h3>
<p>父组件给子组件传数据,这是最基础的操作吧?但有时候父组件数据改了,子组件却还是老样子,比如这样:</p>
<pre><div class="hljs"><code class="lang-js"><!-- 父组件 Parent.vue -->
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">template</span>></span>
<span class="hljs-tag"><<span class="hljs-name">Child</span> <span class="hljs-attr">:user</span>=<span class="hljs-string">"user"</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"changeUser"</span>></span>改名字<span class="hljs-tag"></<span class="hljs-name">button</span>></span>
<span class="hljs-tag"></<span class="hljs-name">template</span>></span>
<span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">setup</span>></span><span class="javascript">
<span class="hljs-keyword">import</span> Child <span class="hljs-keyword">from</span> <span class="hljs-string">'./Child.vue'</span>
<span class="hljs-keyword">let</span> user = { <span class="hljs-attr">name</span>: <span class="hljs-string">'张三'</span> }
<span class="hljs-keyword">const</span> changeUser = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
<span class="hljs-comment">// 踩坑:直接赋值新对象,子组件没反应</span>
user = { <span class="hljs-attr">name</span>: <span class="hljs-string">'李四'</span> }
}
</span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>
</span></code></div></pre>
<p>这时候点按钮,父组件的user确实变了,但子组件就是不刷新。为啥?因为最开始的user虽然是响应式的,但咱们直接给user“换了个对象”,新对象没被Vue的响应式系统“注册”,子组件自然没收到通知。</p>
<p>✅ 解决办法:别直接换对象,改对象里的属性;或者用ref包裹对象(更稳妥)</p>
<pre><div class="hljs"><code class="lang-js"><!-- 父组件 正确写法 -->
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">setup</span>></span><span class="javascript">
<span class="hljs-keyword">import</span> Child <span class="hljs-keyword">from</span> <span class="hljs-string">'./Child.vue'</span>
<span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>
<span class="hljs-comment">// 用ref包裹,不管是改属性还是换对象都没问题</span>
<span class="hljs-keyword">let</span> user = ref({ <span class="hljs-attr">name</span>: <span class="hljs-string">'张三'</span> })
<span class="hljs-keyword">const</span> changeUser = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
<span class="hljs-comment">// 方式1:改属性</span>
user.value.name = <span class="hljs-string">'李四'</span>
<span class="hljs-comment">// 方式2:换对象(ref包裹后,这样也能响应)</span>
<span class="hljs-comment">// user.value = { name: '李四' }</span>
}
</span><span class="hljs-tag"></<span class="hljs-name">script</span>></span></span>
</code></div></pre>
<h3><a id="2_49"></a>场景2:子传父,父组件数据变了但页面没更</h3>
<p>子组件通过emit给父组件传值,父组件接收后数据改了,页面却没反应。这种情况大概率是父组件“手动存值”时,把响应式弄丢了。比如:</p>
<pre><div class="hljs"><code class="lang-html"><span class="hljs-comment"><!-- 子组件 Child.vue --></span>
<span class="hljs-tag"><<span class="hljs-name">template</span>></span>
<span class="hljs-tag"><<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"sendMsg"</span>></span>传消息给父组件<span class="hljs-tag"></<span class="hljs-name">button</span>></span>
<span class="hljs-tag"></<span class="hljs-name">template</span>></span>
<span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">setup</span>></span><span class="javascript">
<span class="hljs-keyword">const</span> emit = defineEmits([<span class="hljs-string">'send-msg'</span>])
<span class="hljs-keyword">const</span> sendMsg = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
emit(<span class="hljs-string">'send-msg'</span>, <span class="hljs-string">'我是子组件的消息'</span>)
}
</span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>
<span class="hljs-comment"><!-- 父组件 Parent.vue 踩坑写法 --></span>
<span class="hljs-tag"><<span class="hljs-name">template</span>></span>
<span class="hljs-tag"><<span class="hljs-name">Child</span> @<span class="hljs-attr">send-msg</span>=<span class="hljs-string">"handleMsg"</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">p</span>></span>{{ msg }}<span class="hljs-tag"></<span class="hljs-name">p</span>></span>
<span class="hljs-tag"></<span class="hljs-name">template</span>></span>
<span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">setup</span>></span><span class="javascript">
<span class="hljs-keyword">import</span> Child <span class="hljs-keyword">from</span> <span class="hljs-string">'./Child.vue'</span>
<span class="hljs-comment">// 踩坑:msg不是响应式的!</span>
<span class="hljs-keyword">let</span> msg = <span class="hljs-string">''</span>
<span class="hljs-keyword">const</span> handleMsg = <span class="hljs-function">(<span class="hljs-params">newMsg</span>) =></span> {
msg = newMsg <span class="hljs-comment">// 数据变了,但页面不刷新</span>
}
</span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>
</code></div></pre>
<p>一目了然吧?父组件的msg就是个普通变量,没有用ref或reactive包裹,改了自然不会触发刷新。</p>
<p>✅ 解决办法:给接收数据的变量加“响应式外套”,用ref包基本类型,用ref/reactive包复杂类型</p>
<pre><div class="hljs"><code class="lang-js"><!-- 父组件 正确写法 -->
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">setup</span>></span><span class="javascript">
<span class="hljs-keyword">import</span> Child <span class="hljs-keyword">from</span> <span class="hljs-string">'./Child.vue'</span>
<span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>
<span class="hljs-comment">// 用ref包裹,变成响应式变量</span>
<span class="hljs-keyword">let</span> msg = ref(<span class="hljs-string">''</span>)
<span class="hljs-keyword">const</span> handleMsg = <span class="hljs-function">(<span class="hljs-params">newMsg</span>) =></span> {
msg.value = newMsg <span class="hljs-comment">// 改完页面就刷新啦</span>
}
</span><span class="hljs-tag"></<span class="hljs-name">script</span>></span></span>
</code></div></pre>
<h3><a id="3_97"></a>场景3:兄弟组件通信,数据传了但页面不更</h3>
<p>兄弟组件之间没直接关系,一般用“事件总线”或“Pinia状态管理”传数据。这里先讲简单的事件总线(Vue3里要自己封装哦),踩坑点还是“响应式”。</p>
<p>比如咱们封装个简单的事件总线:</p>
<pre><div class="hljs"><code class="lang-javascript"><span class="hljs-comment">// eventBus.js</span>
<span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>
<span class="hljs-keyword">const</span> bus = ref(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>())
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useEventBus = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
<span class="hljs-keyword">const</span> on = <span class="hljs-function">(<span class="hljs-params">name, callback</span>) =></span> {
bus.value.set(name, callback)
}
<span class="hljs-keyword">const</span> emit = <span class="hljs-function">(<span class="hljs-params">name, ...args</span>) =></span> {
<span class="hljs-keyword">const</span> callback = bus.value.get(name)
callback && callback(...args)
}
<span class="hljs-keyword">return</span> { on, emit }
}
</code></div></pre>
<p>要是兄弟A传数据给兄弟B时,B组件接收后存到了非响应式变量里,照样不刷新。所以核心还是:<strong>接收数据的变量必须是响应式的!</strong></p>
<pre><div class="hljs"><code class="lang-javascript"><!-- 兄弟A组件 -->
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">setup</span>></span><span class="javascript">
<span class="hljs-keyword">import</span> { useEventBus } <span class="hljs-keyword">from</span> <span class="hljs-string">'./eventBus.js'</span>
<span class="hljs-keyword">const</span> { emit } = useEventBus()
<span class="hljs-keyword">const</span> sendToB = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
emit(<span class="hljs-string">'brother-msg'</span>, <span class="hljs-string">'来自A的消息'</span>)
}
</span><span class="hljs-tag"></<span class="hljs-name">script</span>></span></span>
<!-- 兄弟B组件 正确写法 -->
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">setup</span>></span><span class="javascript">
<span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>
<span class="hljs-keyword">import</span> { useEventBus } <span class="hljs-keyword">from</span> <span class="hljs-string">'./eventBus.js'</span>
<span class="hljs-keyword">const</span> { on } = useEventBus()
<span class="hljs-comment">// 响应式变量存数据</span>
<span class="hljs-keyword">let</span> msg = ref(<span class="hljs-string">''</span>)
on(<span class="hljs-string">'brother-msg'</span>, (newMsg) => {
msg.value = newMsg <span class="hljs-comment">// 页面会刷新</span>
})
</span><span class="hljs-tag"></<span class="hljs-name">script</span>></span></span>
</code></div></pre>
<h3><a id="4Pinia_145"></a>场景4:Pinia状态变了,组件不刷新</h3>
<p>用Pinia管理全局状态时,也可能遇到“状态改了页面不更”的情况,多半是这俩问题:</p>
<ol>
<li>
<p><strong>直接修改Pinia的原始数据,没走响应式</strong>:比如在Pinia里定义了个对象,组件里直接给对象赋值新的,没改属性也没用水合(hydrate)。</p>
</li>
<li>
<p><strong>组件里“解构”状态时,把响应式弄没了</strong>:比如用const { count } = useStore(),这样count就变成普通变量了,改了Pinia里的状态,组件也收不到。</p>
</li>
</ol>
<p>✅ 解决办法:</p>
<pre><div class="hljs"><code class="lang-javascript"><span class="hljs-comment">// Pinia仓库 store.js</span>
<span class="hljs-keyword">import</span> { defineStore } <span class="hljs-keyword">from</span> <span class="hljs-string">'pinia'</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useMyStore = defineStore(<span class="hljs-string">'myStore'</span>, {
<span class="hljs-attr">state</span>: <span class="hljs-function"><span class="hljs-params">()</span> =></span> ({
<span class="hljs-attr">user</span>: { <span class="hljs-attr">name</span>: <span class="hljs-string">'张三'</span> },
<span class="hljs-attr">count</span>: <span class="hljs-number">0</span>
}),
<span class="hljs-attr">actions</span>: {
<span class="hljs-comment">// 改状态最好用actions,更规范</span>
changeName(newName) {
<span class="hljs-keyword">this</span>.user.name = newName <span class="hljs-comment">// 改属性,响应式正常</span>
<span class="hljs-comment">// 或者换对象:this.user = { name: newName }</span>
},
addCount() {
<span class="hljs-keyword">this</span>.count++
}
}
})
</code></div></pre>
<pre><div class="hljs"><code class="lang-javascript"><!-- 组件里正确使用 -->
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">setup</span>></span><span class="javascript">
<span class="hljs-keyword">import</span> { useMyStore } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store.js'</span>
<span class="hljs-keyword">import</span> { storeToRefs } <span class="hljs-keyword">from</span> <span class="hljs-string">'pinia'</span> <span class="hljs-comment">// 关键:用这个解构响应式</span>
<span class="hljs-keyword">const</span> store = useMyStore()
<span class="hljs-comment">// 错误写法:解构后失去响应式</span>
<span class="hljs-comment">// const { count, user } = store</span>
<span class="hljs-comment">// 正确写法1:直接用store.xxx</span>
<span class="hljs-built_in">console</span>.log(store.count)
<span class="hljs-comment">// 正确写法2:用storeToRefs解构,保留响应式</span>
<span class="hljs-keyword">const</span> { count, user } = storeToRefs(store)
<span class="hljs-comment">// 修改状态</span>
<span class="hljs-keyword">const</span> change = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
store.changeName(<span class="hljs-string">'李四'</span>) <span class="hljs-comment">// 调用actions改</span>
store.addCount()
}
</span><span class="hljs-tag"></<span class="hljs-name">script</span>></span></span>
</code></div></pre>
<h2><a id="_200"></a>最后总结个“万能避坑指南”</h2>
<ol>
<li>
<p><strong>数据要“响应式”</strong>:基本类型(字符串、数字等)用ref,复杂类型(对象、数组)用ref或reactive,别用普通变量;</p>
</li>
<li>
<p><strong>修改要“按规矩来”</strong>:ref包裹的改.value,reactive包裹的改属性(别直接换对象),Pinia状态用actions改或storeToRefs解构;</p>
</li>
<li>
<p><strong>接收要“保响应”</strong>:不管是父传子、子传父还是兄弟通信,接收数据的变量必须是响应式的,别直接存到普通变量里。</p>
</li>
</ol>
<p>其实Vue3的响应式没那么玄乎,核心就是让数据被Vue“盯牢”。下次再遇到不刷新的问题,先检查下数据是不是响应式的,修改方式对不对,基本都能解决~</p>