1

在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框架中,refshallowRef的区别以及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多平台发布


Miniwa
29 声望1 粉丝