Vue利用computer解决单项数据流的问题详解

 更新时间:2023年07月03日 16:24:35   作者:子辰Web草庐  
Vue 是一个非常流行和强大的前端框架,它让我们可以用简洁和优雅的方式来构建用户界面,今天我们来分享一个 Vue 中非常经典的问题,也是一个非常实用的技巧,希望对大家有所帮助

Vue 是一个非常流行和强大的前端框架,它让我们可以用简洁和优雅的方式来构建用户界面。

但是,Vue 也有一些需要注意和掌握的细节和技巧。

我是渡一前端子辰老师,今天我们来分享一个 Vue 中非常经典的问题,也是一个非常实用的技巧。

这个问题涉及到 Vue 的一个核心特性:单向数据流。

让人头痛的单向数据流

如果你不了解单向数据流是什么,或者你不知道如何在 Vue 中正确地使用它,那么请继续往下看,我保证你会有所收获。

这个问题在你使用 Vue 去封装一个表单组件的时候,就会非常明显地体现出来。

表单组件是前端开发中非常常见和重要的一种组件,它可以让用户输入和提交数据,从而实现各种功能。

比如说,我们这里我们封装了一个搜索条的简单示例,来以小见大:

这个组件很简单:

<template>
  <el-input v-model="modelValue.keyword" :placeholder="modelValue.placeholder">
    <template #prepend>
      <el-select v-model="modelValue.selectedValue" placeholder="Select" style="width: 85px">
        <el-option v-for="item in modelValue.options" :key="item.value" :label="item.label" :value="item.value"></el-option>
      </el-select>
    </template>
    <template #append>
      <el-button :icon="Search" />
    </template>
  </el-input>
</template>
<script setup>
  import { Search } from '@element-plus/icons-vue';	
  const props = defineProps({
    modelValue: {
      type: Object,
      required: true,
    },
  });
</script>

通过 props 传入一个对象,这个对象里包括所以我们需要的数据,比如:占位符 placeholder、文本框的输入值 keyword、下拉框的选中值 selectedValue、下拉框的选项值 options。

这些数据都给我们,我们将这个界面渲染出来。

那么父组件是这样的:

<template>
  <div>
    <SearchBar v-model="searchData" />
  </div>
</template>
<script setup>
import { ref } from 'vue';
import SearchBar from './components/SearchBar.vue';
const searchData = ref({
  keyword: '',
  placeholder: '请输入你要查询的关键字',
  options: [
    { label: '视频', value: 'video' },
    { label: '文章', value: 'article' },
    { label: '用户', value: 'user' },
  ],
  selectedValue: 'video',
});
</script>

父组件在使用时自然而然会传递一些数据,这个数据也很简单,一看就明白了。

我们使用 v-model 来绑定数据,这样只要这个组件改动了这个数据,那么我们父组件就能收到通知,能够对这个数据做相应的变化。

这个组件结构是非常清晰的,就是这么一种结构:

这都是基础知识,没什么好说的,但是实际情况是什么样的呢?

现在的问题是子组件的文本框使用的是 v-model 绑定数据,但是这一绑定就把父组件传递的属性它里边的数据绑定进去了,那现在就变成了这种结构了:

于是这种情况就打破了单项数据流,打破单向数据流是要付出代价的,打破一次你的工程就距离“ shǐ山”更进一步。

那么我们希望不要打破单项数据流,回归到正常的模式,那该怎么做呢?

解决办法

最笨的办法就是在子组件里不使用 v-model,不然的话文本框一变这个父组件的数据就会跟着变,所以我们把 v-model 拆成原始的形式。

<template>
  <!-- 将 v-model 拆分 -->
  <el-input 
    :modelValue="modelValue.keyword" 
    @update:modelValue="handleKeywordChange" 
    :placeholder="modelValue.placeholder">
    <!-- etc... -->
  </el-input>
</template>
<script setup>
  // etc...
  // 定义一个 emit 事件
  const emit = defineEmits(['update:modelValue'])
  function handleKeywordChange(val) {
    console.log('val >>> ', val)
    // 触发子组件的 update:modelValue 事件
    emit('update:modelValue', {
      // 因为这里我们只是修改了 keyword 的值
      // 所以我们将 props.modelValue 展开之后,单独将 keyword 的值赋值为新的值
      ...props.modelValue,
      keyword: val
    })
  }
</script>

一个是 modelValue 用于绑定值。

另外一个是 update:modelValue 用于监控这个组件的 update 事件,当事件触发的时候调用 handleKeywordChange 函数。

handleKeywordChange 函数,要做的事情就是去触发子组件 update:modelValue 事件,通知父组件去更改数据,所以我们定义了一个 emit 事件,数据变化的时候调用 emit 返回更新的数据。

虽然说这样很麻烦,但是我们保证了单项数据流了。

那么有没有一种简介的方法呢?

其实面对这个问题,Vue 官方也好还是一些第三方库,比如:vueuse 他们都有一种解决办法,就是使用计算属性去给它包一层:

父组件的数据传递过来之后,并没有直接绑定到内部的文本框,而是在中间加了一个计算属性,然后用这个计算属性去绑定这个文本框,这个计算属性要同时设置它的 getter 和 setter,当读这个计算属性的时候,读的其实就是 modelValue 里的东西,所以读是没问题的。

但是这个文本框由于绑定了 v-model 这个文本框会变动的,变动的话改的就是这个计算属性,也就触发了这个计算属性的 setter,那么在 setter 里边我们就可以写代码去触发这个 emit 事件。

这样就简化了代码,同时又保证了单项数据流,官方就是这样建议的,我们去尝试一下好不好用:

<template>
  <!-- 将计算属性绑定到文本框之上 -->
  <el-input v-model="keyword" :placeholder="modelValue.placeholder">
  <!-- etc... -->
  </el-input>
</template>
<script setup>
// etc...
// 定义一个 emit 事件
const emit = defineEmits(['update:modelValue'])
// 写一个计算属性,同时提供 get 和 set 
const keyword = computed({
  // 读取的时候直接返回读取的值
  get() {
    return props.modelValue.keyword;
  },
  // 当修改的时候我们执行 emit 的操作
  set(val) {
    console.log('val >>> ', val)
    emit('update:modelValue', {
      ...props.modelValue,
      keyword: val
    })
  }
})
</script>

这样我们就不需要拆分 v-model 了,虽然有所简化,但是简化的并不多,因为下拉框的选中值 selectedValue、下拉框的选项值 options 都要做成计算属性。

那么我们能不能想一个办法,就是说这个计算属性不要只返回给我们一个字段,而是字节把整个对象返回,像这种模式:

<template>
  <!-- 绑定的时候直接绑定计算属性上的字段 -->
  <el-input v-model="model.keyword" :placeholder="model.placeholder">
    <template #prepend>
      <el-select v-model="model.selectedValue" placeholder="Select" style="width: 85px">
        <el-option v-for="item in model.options" :key="item.value" :label="item.label" :value="item.value"></el-option>
      </el-select>
    </template>
    <template #append>
      <el-button :icon="Search" />
    </template>
  </el-input>
</template>
<script setup>
  import { Search } from '@element-plus/icons-vue';
  const props = defineProps({
    modelValue: {
      type: Object,
      required: true,
    },
  });
  const emit = defineEmits(['update:modelValue'])
  const model = computed({
    get() {
      return props.modelValue;
    },
    set(val) {
      emit('update:modelValue', val)
    }
  })
</script>

将来修改计算属性的时候就触发事件,绑定值得到话就绑定计算属性的字段。

这样就能通过一个计算属性属性,搞定全部的问题了。

但是现在修改是无效的,因为绑定的是 model 里的一个字段,并不是 model,所以修改的也是 model 的字段,所以并不会触发 set 的更新:

因为只有改动了 model 本身的时候,它才会去运行 setter,改动的是某一个字段就不会运行 setter,那现在就不好办了。

但是,转折来了,有一个奇招可以解决这个问题:

const model = computed({
  get() {
    // 我们这里返回一个代理对象,代理 props.modelValue 这个属性
    return new Proxy(props.modelValue, {
      // 因为这是一个代理对象,那么将来修改代理对象的某个值时
      // 就会运行这个 set 函数
      // 函数中可以拿到 
      //   obj:改动的对象 
      //   name:改动的属性名 
      //   val:改动的属性值
      set(obj, name, val) {
        console.log('Emit >>> ', name, val)
        // 当我们想改的一个对象的属性时并不去真正的修改
        // 而是在这里也触发 emit,然后生成一个新的对象
        emit('update:modelValue', {
          ...obj, // 展开以前对象的值
          [name]: val // 将其中的修改的属性修改为新的值
        })
        return true; // 最后返回一个 true
      },
    });
  },
  set(val) {
    emit('update:modelValue', val)
  }
})

这就正常的触发了事件函数,那么这样一来代码就进一步得到简化了。

我们使用一个计算属性属性就可以替代里边的所有字段,特别是在一个大表单里,有很多很多的字段,这一招非常的好用。

在子组件里无论有多少个文本框选项,都去用这个计算属性去绑定就可以了。

既不会打破单向数据流,而且实现代码也非常少。

扩展

其实我们还可以把这个问题扩展一下,因为我们在实际开发中,封装表单是一件常事,所以在每一次封装表单的都是都去写一次这样的代码有点繁琐,我们可以把它提出去,写成一个辅助函数。

import { computed } from 'vue';
/**
 * props:属性对象
 * propName:要做成计算属性的名字
 * emit:emit 函数
 */
export function useVModel(props, propName, emit) {
  return computed({
    get() {
      return new Proxy(props[propName], {
        set(obj, name, val) {
          console.log('emit', name, val);
          emit('update:' + propName, {
            ...obj,
            [name]: val,
          });
          return true;
        },
      });
    },
    set(val) {
      emit('update:' + propName, val);
    },
  });
}

这样就可以通过一个辅助函数帮我们把要做的事情实现,使用起来就非常的舒服了:

<template>
  <el-input v-model="model.keyword" :placeholder="model.placeholder">
    <template #prepend>
      <el-select v-model="model.selectedValue" placeholder="Select" style="width: 85px">
        <el-option v-for="item in model.options" :key="item.value" :label="item.label" :value="item.value"></el-option>
      </el-select>
    </template>
    <template #append>
      <el-button :icon="Search" />
    </template>
  </el-input>
</template>
<script setup>
  import { Search } from '@element-plus/icons-vue';
  import { useVModel } from './useVModel'; // 导入辅助函数
  const props = defineProps({
    modelValue: {
      type: Object,
      required: true,
    },
  });
  const emit = defineEmits(['update:modelValue']);
  // 调用函数将需要的参数传递进去
  const model = useVModel(props, 'modelValue', emit);
</script>

以后无论是非常简单的表单封装,还是非常庞大的表单封装,都可以用这么几行代码来解决问题了。

既保护了单项数据流,又简化了代码的书写,在实际开发中用起来是非常的好用。

总结

通过这篇文章,你应该对 Vue 的单向数据流有了更深入的理解和掌握。

你学习了如何在封装表单组件时避免打破单向数据流,以及如何使用计算属性和辅助函数来简化和优化你的代码。

这些技巧不仅能让你写出更高质量和更易维护的代码,还能让你提高你的开发效率和水平。

以上就是Vue利用computer解决单项数据流的问题详解的详细内容,更多关于Vue单项数据流的资料请关注脚本之家其它相关文章!

相关文章

  • HTML页面中使用Vue示例进阶(快速学会上手Vue)

    HTML页面中使用Vue示例进阶(快速学会上手Vue)

    Vue是用于构建用户界面的渐进式JavaScript框架。特色:构建用户界面—数据变成界面;渐进式—Vue可以自底向上逐层的应用。VUE有两种使用方式,一种实在html中直接使用vue做开发,一种是企业级的单页面应用。
    2023-02-02
  • element ui时间日期选择器el-date-picker报错Prop being mutated:"placement"解决方式

    element ui时间日期选择器el-date-picker报错Prop being mutated:"

    在日常开发中,我们会遇到一些情况,限制日期的范围的选择,下面这篇文章主要给大家介绍了关于element ui时间日期选择器el-date-picker报错Prop being mutated: "placement"的解决方式,需要的朋友可以参考下
    2022-08-08
  • vite5+vue3+ import.meta.glob动态导入vue组件图文教程

    vite5+vue3+ import.meta.glob动态导入vue组件图文教程

    import.meta.glob是Vite提供的一个特殊功能,它允许你在模块范围内动态地导入多个模块,这篇文章主要给大家介绍了关于vite5+vue3+ import.meta.glob动态导入vue组件的相关资料,需要的朋友可以参考下
    2024-07-07
  • Vue中使用flv.js播放视频的示例详解

    Vue中使用flv.js播放视频的示例详解

    这篇文章主要为大家详细介绍了如何在Vue项目中使用flv.js播放视频,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2024-04-04
  • 如何使用Vue mapState快捷获取Vuex state多个值

    如何使用Vue mapState快捷获取Vuex state多个值

    这篇文章主要为大家介绍了如何使用Vue mapState快捷获取Vuex state多个值实现详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-06-06
  • vue Element左侧无限级菜单实现

    vue Element左侧无限级菜单实现

    这篇文章主要介绍了vue Element左侧无限级菜单实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-06-06
  • vue调试工具没有Pinia模块的简单解决办法

    vue调试工具没有Pinia模块的简单解决办法

    Pinia是Vue的存储库,它允许您跨组件/页面共享状态,这篇文章主要给大家介绍了关于vue调试工具没有Pinia模块的简单解决办法,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2024-02-02
  • 基于Vue-cli的一套代码支持多个项目

    基于Vue-cli的一套代码支持多个项目

    这篇文章主要介绍了基于Vue-cli的一套代码支持多个项目的方案,帮助大家更好的理解和学习使用vue框架,感兴趣的朋友可以了解下
    2021-03-03
  • 基于Vue3和Element Plus实现可扩展的表格组件

    基于Vue3和Element Plus实现可扩展的表格组件

    在开发过程中,我们经常需要创建具有复杂功能的表格组件,本文将介绍如何使用 Vue 3 和 Element Plus 库来构建一个可扩展的表格组件,文中有详细的代码示例供大家参考,需要的朋友可以参考下
    2024-07-07
  • Vue监听事件实现计数点击依次增加的方法

    Vue监听事件实现计数点击依次增加的方法

    今天小编就为大家分享一篇Vue监听事件实现计数点击依次增加的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-09-09

最新评论