1. 问题复现

我们以 Naive UI 框架库中的 NDrawer 组件 为例,这个组件可以通过 v-model:show="" 的方式控制抽屉的开关:

表单组件值传递的完美解决方案,表单的双向绑定的思考,嵌套 v-model 解决 Unexpected mutation of “XXX“ prop `vue/no-mutating-pro_前端


不过我们需要对这个组件进行二次封装来满足更多的需求,我的封装代码如下:

表单组件值传递的完美解决方案,表单的双向绑定的思考,嵌套 v-model 解决 Unexpected mutation of “XXX“ prop `vue/no-mutating-pro_javascript_02


我们可以发现报错如下(需要安装 ESLint 插件,VSCode 才会显示行内错误):写项目请一定要打开eslint校验,它不仅仅是一款代码格式规范的一个检验,更多的是语法层面会很有启发

表单组件值传递的完美解决方案,表单的双向绑定的思考,嵌套 v-model 解决 Unexpected mutation of “XXX“ prop `vue/no-mutating-pro_封装_03


即,我们对 prop 的内容进行了修改,违反了单向数据流原则。

2. 问题分析

v-model 仅仅是一个语法糖而已,它的原理是:父组件通过 props 传入变量,子组件通过事件把更新后的变量值 emit 出来,再由父组件进行事件处理。

所以实质上,在子组件内,我们并不可以直接将 prop 的变量应用于子组件深层次组件的 v-model 上(因为 v-model 会隐含的对 prop 值更新),故上图的用法是肯定会触发错误的。

​​单项数据传递不知何物可以阅读一下​​

3. 问题解决

子组件在父组件中的调用代码如下:(先总

表单组件值传递的完美解决方案,表单的双向绑定的思考,嵌套 v-model 解决 Unexpected mutation of “XXX“ prop `vue/no-mutating-pro_前端_04


解决的方法也非常简单,以上图为例,我们仅需要将 openDrawer 通过中间变量进行 v-model 化,再把中间变量传入子组件深层次组件的 v-model 即可:(后分)

表单组件值传递的完美解决方案,表单的双向绑定的思考,嵌套 v-model 解决 Unexpected mutation of “XXX“ prop `vue/no-mutating-pro_vue.js_05


表单组件值传递的完美解决方案,表单的双向绑定的思考,嵌套 v-model 解决 Unexpected mutation of “XXX“ prop `vue/no-mutating-pro_前端_06


我们利用一个计算属性 showDrawer 来实现 v-model,将其 getter 设置为从 prop 的 openDrawer 初始化,而 setter 的内容就是将新的值 emit 出去。

这样,showDrawer 这个中间变量(计算属性)便可以应用到 NDrawer 的 v-model 上了。

5、重点上述的1、2、3、4为封装一个完美表单组件提供了非常大思路

​​建议阅读computed的详细用法​​

表单组件值传递的完美解决方案,表单的双向绑定的思考,嵌套 v-model 解决 Unexpected mutation of “XXX“ prop `vue/no-mutating-pro_封装_07


见了太多的表单封装,结构基本都是这样(内部代码对于数据的传递很乱,但是项目可以跑,数据传递乱就乱在数据的单项数据流),第三方组件库的表单内部肯定是对数据绑定的单项传递有一个完美的规划的。

然后普通做法,就是基于第三方组件库,封装一个表单组件,然后prop传值过去,自己封装的表单组件进行接收值,然后直接就v-modal到表单项上(代码完全可以跑,没有问题,但是不完美,总感觉有个瑕疵)。

瑕疵在何处?

传过去的prop直接以双向绑定的方式绑定过去,是不妥的,双向绑定呀,其实已经在子组件非法的在改变父组件传过去的prop了

做法:随便一个demo,思路草稿(vue是条路,数据是来来往往的人)

//封装的子组件在父组件中调用
<form-qr
v-model:formData="formData"
></form-qr>
//子组件
<template>
<div class="form-container">
<el-form
ref="ref_form"
:model="myFormData"
:rules="formRules"
class="form-item-container"
label-width="130px"
>
<div class="form-row">
<el-form-item label="工作坊" prop="workShopArrValue">
<el-cascader
v-model="myFormData.workShopArrValue"
:options="workShop"
:show-all-levels="false"
@change="valueChangeWorkshop"
>
</el-cascader>
</el-form-item>
<el-form-item
label="项目编号"
prop="project_num"
class="form-item-projectid"
>
<el-input
v-model.trim="myFormData.project_num"
placeholder="请输入项目编号"
disabled
@input="filterId"
></el-input>
</el-form-item>
</div>
</el-form>
</div>
</template>

<script>
import { ref, reactive, watchEffect, watch, computed, inject } from "vue";
export default {
props: {
formData: {
type: Object,
default: () => {
return {};
},
},
},
emits: ["update:formData"],
setup(props, { emit }) {
const myFormData = computed({
get: () => props.formData;
set: (obj) => { emit('update:formData', obj)}
});
}
</script>

思路代码未去实现,计算属性没有去处理过复杂的对象,如果计算属性不行就用watch就行了。数据传到子组件,然后将改动的消息传到父组件,父组件来改动数据