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

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 事件。

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

<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>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0S9ZMcIZ-1687785872723)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0eeffdb9a5a748e4997da291ec01b8f5~tplv-k3u1fbpfcp-zoom-1.image)]

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

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

总结

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

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

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

希望你能在你的项目中运用这些技巧,让你的 Vue 组件更加高效!

感谢你阅读本文,如果你有任何疑问或建议,请在评论区留言,如果你觉得这篇文章有用,请点赞收藏或分享给你的朋友!

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值