在vue3中如果我们需要获取一个响应式的变量,可以使用ref来定义一个变量。
const name = ref( "" );
name.value = "test"
定义好后,就可以实现修改状态,更新UI的效果了。
在这个基础上,本文主要讨论跨组件时如何管理Ref的状态,以及如何更好地封装Ref的读写。
单向数据流
https://cn.vuejs.org/guide/components/props#one-way-data-flow
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
举一个例子,我们要实现一个header里面有一个menu的按钮,当点击了会把侧边栏滑进来,然后点击了关闭按钮又隐藏。
<script>
import { ref } from "vue";
import Header from "./components/Header.vue";
import Nav from "./components/Nav.vue";
export default {
components: {
Header,
Nav,
},
setup() {
const isOpen = ref(false);
const handToggle = () => isOpen.value = !isOpen.value;
return { isOpen, handToggle };
},
};
</script>
<template>
<Header @toggle="handToggle" />
<Nav :isOpen="isOpen" @toggle="handToggle" />
</template>
header.vue
<script>
export default {
setup(props, { emit }) {
const handleMenu = () => {
emit("toggle");
};
return { handleMenu };
},
};
</script>
<template>
<header>
<nav>
<button @click="handleMenu">menu</button>
</nav>
</header>
</template>
nav.vue
<script>
export default {
props: {
isOpen: {
type: Boolean,
default: false,
},
},
setup(props, { emit }) {
const handleMenu = () => {
emit("toggle");
};
return { props, handleMenu };
},
};
</script>
<template>
<div :class="['nav', { open: props.isOpen }]">
<a @click="handleMenu">close</a>
</div>
</template>
整体的效果如下所示:
在点击了Header.vue里面的Menu 后发送一个emit给上层的component,透过上层接收到的toggle事件去修改我们isOpen的value,然后isOpen再透过props 传入Nav.vue里面去控制侧边选单的开起,然后透过侧边选单的close按钮,在发送一个 toggle事件往上去更改 isOpen,然后props 的isOpen也会同步知道被更改,这样就完成了整个的流程!
整个流程其实也遵循了单一数据流的特性。props都是从父级修改的。但是不是感觉把简单的事情复杂化了?增加了一层事件的往返通信来修改props。有没有更方便的做法?其实我们可以直接去掉事件这一层。
<script>
// 其他省略...
export default {
setup() {
const isOpen = ref(false);
const handleOpenMenu = () => isOpen.value = !isOpen.value;
return { isOpen, handleOpenMenu };
},
};
</script>
<template>
<Header :handleOpenMenu="handleOpenMenu" />
<Nav :isOpen="isOpen" :handleOpenMenu="handleOpenMenu" />
</template>
把修改值的方法传递给子组件去调用,避免了事件的通信。调整后的数据流就简单多了。
单向数据流是一种数据流动的模式,数据从父组件流向子组件,子组件只能通过props从父组件接收数据,而不能直接修改父组件的状态。这种模式使得数据流更加清晰和可预测,有助于维护和管理大型应用。
在react中也有类似的效果"Lifting State Up"(状态提升),当多个组件需要共享同一个状态时,可以将状态提升到它们共同的父组件中。这样,子组件可以通过props从父组件获取状态,并通过回调函数通知父组件更新状态。状态提升有助于避免组件间的直接状态共享,使得状态管理更加集中和一致。
封装
你可能会疑惑,上面子组件调用handleOpenMenu不是直接修改props么?这种不属于直接修改,控制权是在父组件。我们把修改的方式用函数封装起来了,隐藏了中间的实现细节。这就是一种封装。最典型的例子就是react的useState。
import { useState } from 'react' ;
function MyComponent () {
const [age, setAge] = useState ( 28 );
const [name, setName] = useState ( 'Taylor' );
const [address, setAddress] = useState ( 'Taiwan' );
}
在React中,useState允许开发者创建一个状态变量和一个设置这个状态的函数。这样,开发者可以通过这个函数来修改状态,而不是直接操作状态变量。这种方式简化了状态管理,并且使得状态的修改更加可控。
虽然在小型项目中直接使用.value来修改Vue中的响应式数据非常方便,但在需要传递props或者使用emit进行父子组件通信时,还是需要定义设置函数。随着项目的增大或者时间的推移,可能会出现同时使用.value和设置函数的情况,这可能会让代码变得混乱。所以最好是我们统一封装规范它的使用,我们封装一个useRefState来管理状态。
import { shallowRef } from "vue" ;
export function useRefState ( baseState ) {
const state = shallowRef (baseState);
const update = ( newValue ) => {
state. value = newValue;
};
return [state, update];
}
这么一来我们就可以在开发的时候使用跟React 一样的方式来定义状态。
< script setup > import { useState } from "./composables/useRefState.js" ;
const [name, setName] = useRefState ( "mike" );
const [info, setInfo] = useRefState ({
name : "mike" ,
age : 12 ,
});
</ script >
< template >
< h2 > name: {{ name }} </ h2 >
< pre > info: {{ info }} </ pre >
< input type = "text" v-model = "name" />
< button @ click = "setName('jacky')" > set name </ button >
< button @ click = "setInfo({ name: 'andy', age: 20 })" > set info </ button >
</ template >
它的好处是可以减少每次在定义ref的时候都要再写一个set function ,造成code 会很多很杂的问题,而且也不会失去响应式的特性!
在上面的实现中,我们使用的是shallowRef而非ref。
这段内容主要解释了在Vue框架中,ref
和shallowRef
的区别以及shallowRef
的用途。ref
是Vue中的一个响应式API,它将内部值转换为响应式对象。与ref
不同,shallowRef
内部的值不会自动转换为响应式。只有当你通过.value
属性设置值时,这个操作才是响应式的。也就是说,只有第一层的属性是响应式的。
shallowRef
常用于大型数据结构的性能优化和与外部状态管理系统的整合。由于shallowRef
只在第一层属性上是响应式的,这使得它在处理大型或深层数据结构时,性能更好。因为不需要对每个深层属性都进行响应式处理,减少了性能开销。
当我们使用useRefState之后,就不能直接针对单个属性去set value,需要将原本的物件加上新的值一起写入。
const [info, setInfo] = useState ({
name : "mike" ,
age : 12 ,
});
// 把所有属性一起写入
setInfo ({
...info,
address : addr,
});
基本上就跟我直接替换整个object 一样,所以这边就不需要使用ref来增加无谓的效能消耗。
总结
通过本文的探讨,我们可以看到Vue 3的响应式系统为我们提供了强大的工具来管理跨组件的状态。无论是通过事件通信还是直接传递方法来更新状态,Vue都提供了灵活的解决方案。封装useRefState的做法,让我们能够以一种更加接近React的useState的方式来处理Vue中的状态,使得状态管理更加直观和一致。
最后,无论我们选择哪种方式来管理状态,重要的是要确保我们的应用遵循清晰的数据流和一致的设计模式。这样,随着项目的增长和复杂性的增加,我们仍然能够保持代码的可维护性和可扩展性。
本文由mdnice多平台发布
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。