前言
这是一个之前 2023年12月月底碰到的一个问题
这个问题还是 比较复杂, 呵呵 这个在当时 看来 我甚至觉得 我可能搞不定这个问题
但是 当时出现了一些 其他的可以临时解决这个问题的方式, 因此 当时就没有深究
然后 过了两天 重新复现了一下 问题, 重新看了一下 这个问题, 发现 这个问题 然后 发现一个很奇怪的现象
就是 父子组件拿到的 model 不同步, 这里的 udm-form 是从业务这边拿到 一个 model, 然后 业务这边做了一些 更新之类的操作, 然后 udm-input 这边拿到的 model 却还是原来的数据
udm-form -- provide model
el-form
el-row
el-col
el-form-item
udm-input -- inject model
input
然后 直到最近 2024年1月月底又重新来看一下这个问题, 然后 简化一下问题
重新复现 一下这个问题 花了一天的时间
如下的测试用例 有点长, 问题 也比较复杂, 这个 排查的过程也是相当复杂的, 这里仅仅是 看一下 这个问题的流程, 以及一些解决方式
当然 这个也主要是和 组件的设计问题 息息相关
测试用例
业务组件
<template>
<div class="testParent" >
<!-- <el-input v-model="checkItem" ></el-input>-->
<!-- <UserDefinedModelInput name="checkItem" v-model="checkItem" ></UserDefinedModelInput>-->
<div v-for="dateItem in dateItemList" >
<div v-for="subItem in dateItem.children" >
<UserDefinedModelParent :model.sync="subItem.model" >
<!-- <el-input v-model="subItem.model.name" ></el-input>-->
<UserDefinedModelInput name="name" v-model="subItem.model.name" :stringAttr="subItem.model.name" ></UserDefinedModelInput>
</UserDefinedModelParent>
</div>
</div>
</div>
</template>
<script>
import UserDefinedModelInput from '../packages/input/src/UserDefinedModelInput';
import UserDefinedModelParent from '../packages/input/src/UserDefinedModelParent';
export default {
name: 'App',
components: {
UserDefinedModelInput,
UserDefinedModelParent,
},
data() {
return {
checkItem: 'check',
dateItemList: [
{
date: '2024-01-22',
children: [
{
biz: 'math',
model: {
name: 'check'
}
}
]
}
],
};
},
computed: {},
created() {
},
mounted() {
let _this = this;
setTimeout(function() {
_this.checkItem = 'updated check';
// case1, direct update
// _this.dateItemList[0].children[0].model.name = 'updated check';
// case2, set [], then update first element, Vue not recreate UserDefinedModelInput Element, and didnt' update injected model
_this.dateItemList = []
_this.dateItemList.push({date:'2024-01-23',children:[{biz:'math',model:{name:'updated check'}}]})
console.log(_this.dateItemList[0].children[0].model);
// case3, set[], then update first element delay, Vue recreate UserDefinedModelInput Element
// _this.dateItemList = []
// setTimeout(function() {
// _this.dateItemList.push({date:'2024-01-22',children:[{biz:'math',model:{name:'updated check'}}]})
// console.log(_this.dateItemList[0].children[0].model);
// }, 500)
}, 5000);
},
methods: {
handleClick($event, item) {
console.log(' clicked item ', $event, item);
}
}
};
</script>
<style>
</style>
功能父组件
这里主要是为了 provide 一个 model 出来, 构造 问题现场
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'UserDefinedModelParent',
provide() {
return {
model: this.currentModel
};
},
computed: {
currentModel: {
get() {
return this.model;
},
set(val) {
this.$emit('update:model', val);
},
},
},
props: {
model: {
type: Object,
default: () => {
return {};
}
}
}
};
</script>
<style scoped>
</style>
功能子组件
<template>
<el-input ref="comp"
:class="[this.align, this.stringAttr]"
:type="currentType"
:clearable="clearable"
:rows="rows"
:maxlength="currentMaxLength"
:show-word-limit="showWordLimit"
v-model="fieldValue"
v-bind="$attrs"
v-on="$listeners"
@focus="myFocus" @change="change" @clear="_clear"
>
<template v-slot:prefix>
<slot :name="prefix">
<i v-if="prefix" class="slot" :class="prefix"></i>
</slot>
</template>
<template v-slot:suffix>
<slot :name="suffix">
<i v-if="suffix" class="slot" :class="suffix"></i>
</slot>
</template>
<template v-slot:prepend>
<slot :name="prepend">
<i v-if="prepend" class="slot" :class="prepend"></i>
</slot>
</template>
<template v-slot:append>
<slot :name="append">
<i v-if="append" class="slot" :class="append"></i>
</slot>
</template>
</el-input>
</template>
<script>
import Base from './UserDefinedModelBase';
export default {
name: 'UserDefinedModelInput',
props: {
value: [String, Number],
maxlength: [Number, String],
showWordLimit: {
type: Boolean,
default: false
},
type: String,
textarea: Boolean,
rows: Number,
prefix: String,
suffix: String,
prepend: String,
append: String,
align: String,
enter: {
type: Boolean,
default: true
},
stringAttr: String
},
computed: {
},
data() {
return {
currentType: this.type === 'number' ? '' : this.type,
currentMaxLength: '',
ifEnter: false
};
},
created() {
console.log(" UserDefinedModelInput is created ... ")
if (this.textarea) {
this.currentType = 'textarea';
}
if (this.maxlength) {
this.currentMaxLength = this.maxlength;
} else if (this.maxlength === undefined) {
if (this.currentType === 'textarea') {
this.currentMaxLength = 10000;
} else {
this.currentMaxLength = 50;
}
}
},
mixins: [Base],
mounted() {
this.extendMethods(this.$refs.comp, ['focus', 'blur', 'select']);
},
watch: {
fieldValue: {
deep: true,
handler(newVal, oldVal) {
this.$nextTick(() => {
if (this.ifChanged || this.$attrs.disabled) {
if (newVal !== oldVal) {
this.confirm();
}
}
});
}
}
},
methods: {
myFocus() {
this.placeholder = '';
console.log(this.model)
},
change() {
this.ifChanged = true;
this.confirm();
},
_clear() {
this.ifChanged = false;
},
}
};
</script>
<style lang="scss" scoped>
</style>
功能子组件依赖的 Base.js
import {cloneDeep, isEqual, set} from 'lodash';
import {extendMethods, getLabelByValue} from './index';
// import {mapActions} from "vuex";
export default {
inject: {
model: {
default: null
}
},
props: {
value: [String, Number],
label: String,
name: String,
defaultValue: [String, Array, Object, Date, Boolean, Number],
keyMap: {
type: Object,
default: () => {
return {};
}
},
clearable: {
type: Boolean,
default: true
},
/**
* 是否选中array第一个
* */
defaultFirst: {
type: Boolean,
default: false
},
required: {
type: Boolean,
default: false
},
confirmByChange: {
type: Boolean,
default: true
},
separator: {
type: String,
default: ','
}
},
watch: {
},
data() {
return {
ifChanged: false,
loading: false,
getLabelByValue,
refreshKey: null,
keyMaps: {
label: 'label',
value: 'value',
...this.keyMap
}
};
},
computed: {
fieldValue: {
get() {
if (this.model) {
if (this.model[this.name] !== undefined) {
return this.model[this.name];
}
return this.getDefaultValue();
} else {
return this.value || this.getDefaultValue();
}
},
set(val) {
if (this.model) {
const model = cloneDeep(this.model);
set(model, this.name, val);
if (!isEqual(this.model, model)) {
set(this.model, this.name, val);
}
} else {
this.$emit('input', val);
}
}
}
},
created() {
},
methods: {
extendMethods(ref, names = []) {
return extendMethods(this, ref, names);
},
getDefaultValue() {
return this.value || '';
}
}
};
问题的现象是 假设我把业务组件中的 UserDefinedModelInput 换成 el-input, 测试用例中的更新是可以正常同步到视图的
假设吧 el-input 更新成 UserDefinedModelInput 可以看到 视图就不会动态更新了, 这个一直 让我很疑惑, 因为我这里也使用了 v-model 绑定 subItem.model.name 按道理来说 父子组件应该同步的基本上都会同步呢
但是 实际情况不是, 我们这里 来看一下
inject 变量的初始化
这个过程是在 Vue组件 初始化的时候, 会执行的, 采集这部分的 inject 变量
从这里上下文可以看到这里 model 的数据是从 Parent 中获取的
然后这里 result 是新建的一个对象 然后对象是从 Parent 中获取的
问题的调试?
在子组件 打上一个断点看一下上下文
可以看到的是 子组件的 model 还是原来的, 父组件的 model 是更新之后的
然后 子组件中和 el-input 绑定的 fieldValue 的取值是来自于 inject 的 model
这个不同步 和 provide, inject 机制有关系, inject 的变量会在 VueComponent 的时候 初始化一次, 并且inject外层是 vue 这边新建的一个对象
通过 v-model 绑定的 value 的数据是被 Vue 这边动态更新了的
这个问题的一个比较核心的关键点 也就在这里, 所以 一些解决的方式 也是围绕着这个来的
根据上面 inject 变量的初始化, 初始化为 最开始的 _this.dateItemList[0].children[0]
然后后面业务这边执行 清空, 然后 push 了一个新的元素, Vue 这边判断这个 UserDefinedModelInput 可以不用重新创建, 因为 UserDefinedModelInput 中的 v-model 和 stringAttr Vue 这边自己会主动的去做响应式更新, 比如 如下看到的 value 的值, 以及 el-input 的 dom 上面的 class 的数据
但是 会造成的差异就是这里 父组件的 model 响应式更新了, 但是 子组件这边没有重新创建, 然后依然保留的是更新之前的 _this.dateItemList[0].children[0] 其值还是为为 “checked”
所以 以上这些就是 解决问题的一些 思考的地方
解决方式一 在原来父子组件共同的模型上面修改
在业务组件中 注释掉当前的 case2 的代码, 放开 case1 的代码
这种方式保证的是 父子组件 provide 和 inject 的对象一直指向同一个对象, 进而 确保了 数据 -> 视图 的正确同步
解决方式二 XXInput 的回显数据绑定到 具体的 value, 不要绑定到 inject 的变量 model 上面
首先 回退之前的解决方式的更新代码, 复现问题
在这里 就是去掉 XXParent 中的 model 的配置, 让其为 null 即可, Base.js 这边 fieldValue 的计算 自动回去获取 value 的数值, 即我们通过 v-model 绑定的数据
将 <UserDefinedModelParent :model.sync="subItem.model" > 更新为 <UserDefinedModelParent >
解决方式三 主动触发 XXInput 的重建
首先 回退之前的解决方式的更新代码, 复现问题
这个就是 尝试更新父组件的相关信息, 尝试在数据模型更新的时候 主动让 vue 这边重新创建相关的 子组件, 这里更新为
将 <div v-for="dateItem in dateItemList" > 更新为 <div v-for="dateItem in dateItemList" :key="dateItem.date" >
通过日志可以看到
注释掉目前的 case2, 放开 case3, 道理是一个, 只是用的另外一种 曲线救国的方式
解决方式四 调整组件的设计
这已经是 简化之后的代码, 但是 还是能够明显看到 诸多问题
从新 简化设计一下 问题同样就解决了
具体的 调整方式, 这里不详细谈了, 太多的需要改进的地方了
解决方式五 使用开源组件
自定义组件本身 多多少少不靠谱, 直接使用 el-input 吧, 同样能够解决问题
这个取值是直接 取得 value 的数据, 不是取自 inject 的变量, 基础的 数据视图同步 方式和 方式二 一样
注释掉 <UserDefinedModelInput name="name" v-model="subItem.model.name" :stringAttr="subItem.model.name" ></UserDefinedModelInput>
放开 <el-input v-model="subItem.model.name" ></el-input>
完