Vue利用computed解决单项数据流的问题
作者:子辰Web草庐
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 组件更加完美和高效!
以上就是Vue利用computed解决单项数据流的问题的详细内容,更多关于Vue computed解决单项数据流的资料请关注脚本之家其它相关文章!