50 基于 provide/inject 属性的模型视图不同步问题

news2024/11/27 12:39:38

前言

这是一个之前 2023年12月月底碰到的一个问题

这个问题还是 比较复杂, 呵呵 这个在当时 看来 我甚至觉得 我可能搞不定这个问题

但是 当时出现了一些 其他的可以临时解决这个问题的方式, 因此 当时就没有深究

然后 过了两天 重新复现了一下 问题, 重新看了一下 这个问题, 发现 这个问题 然后 发现一个很奇怪的现象

83ca39a7fc744275ae00438eb9997964.png

 

就是 父子组件拿到的 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, 测试用例中的更新是可以正常同步到视图的

f492510feaba4250a080222fd666e951.png

 

假设吧 el-input 更新成 UserDefinedModelInput 可以看到 视图就不会动态更新了, 这个一直 让我很疑惑, 因为我这里也使用了 v-model 绑定 subItem.model.name 按道理来说 父子组件应该同步的基本上都会同步呢

但是 实际情况不是, 我们这里 来看一下

f6d46501ef4c44038bff5e57edfcb073.png

 

 

inject 变量的初始化

这个过程是在 Vue组件 初始化的时候, 会执行的, 采集这部分的 inject 变量  

从这里上下文可以看到这里 model 的数据是从 Parent 中获取的

然后这里 result 是新建的一个对象 然后对象是从 Parent 中获取的

5c523a467cf24f749046285f561fa223.png

 

 

问题的调试?

在子组件 打上一个断点看一下上下文

可以看到的是 子组件的 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” 

所以 以上这些就是 解决问题的一些 思考的地方

453ba1a7edf54abe89d4452bcb485e19.png

 

 

解决方式一 在原来父子组件共同的模型上面修改

在业务组件中 注释掉当前的 case2 的代码, 放开 case1 的代码

这种方式保证的是 父子组件 provide 和 inject 的对象一直指向同一个对象, 进而 确保了 数据 -> 视图 的正确同步

d1394ae0b7a24002b938466254a81ff1.png

 

 

解决方式二 XXInput 的回显数据绑定到 具体的 value, 不要绑定到 inject 的变量 model 上面

首先 回退之前的解决方式的更新代码, 复现问题

在这里 就是去掉 XXParent 中的 model 的配置, 让其为 null 即可, Base.js 这边 fieldValue 的计算 自动回去获取 value 的数值, 即我们通过 v-model 绑定的数据

将 <UserDefinedModelParent :model.sync="subItem.model" > 更新为 <UserDefinedModelParent >

1d22436f03934f9ea3f6a1cf1069be44.png

 

 

解决方式三 主动触发 XXInput 的重建

首先 回退之前的解决方式的更新代码, 复现问题

这个就是 尝试更新父组件的相关信息, 尝试在数据模型更新的时候 主动让 vue 这边重新创建相关的 子组件, 这里更新为

将 <div v-for="dateItem in dateItemList" > 更新为 <div v-for="dateItem in dateItemList" :key="dateItem.date" >

通过日志可以看到

c242e423090b441ebb946198e721baaf.png

 

注释掉目前的 case2, 放开 case3, 道理是一个, 只是用的另外一种 曲线救国的方式

2f122fd22fc54dc98936f9d3756039b1.png

 

 

解决方式四 调整组件的设计

这已经是 简化之后的代码, 但是 还是能够明显看到 诸多问题

从新 简化设计一下 问题同样就解决了

具体的 调整方式, 这里不详细谈了, 太多的需要改进的地方了

 

 

解决方式五 使用开源组件

自定义组件本身 多多少少不靠谱, 直接使用 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>

06cf5f9903904c73b167cb612c298c1a.png

 

 

完 

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1556888.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023年第十四届蓝桥杯大赛软件类省赛C/C++研究生组真题(代码完整题解)

C题-翻转⭐ 标签:贪心 简述:如果 S 中存在子串 101 或者 010,就可以将其分别变为 111 和 000,操作可以无限重复。最少翻转多少次可以把 S 变成和 T 一样。 链接: 翻转 思路:要求步骤最少->S每个位置最多修改一次->从头开始遍历不匹配就翻转->翻转不了就-1 …

CVE-2023-38408漏洞修复 - 升级openssl和openssh

CVE-2023-38408 OpenSSH 代码问题漏洞修复 - 升级openssl和openssh ※ 重要说明&#xff1a; 1、升级后会导致无法用ssh远程登录&#xff0c;提示“Permission denied, please try again.” 2、解决方案请查看本章节【三、解决升级后无法用ssh远程登录】 目录 CVE-2023-38408 O…

集合(ArrayList,HashMap,HashSet)详解+ entrySet的应用

集合 例题引入——直线题意分析根据下面的参考代码&#xff0c;自己模仿的参考代码&#xff08;加一点点我的小tips&#xff09; 1.java集合引入2.为什么要使用集合&#xff1f;3.List、Set、Queue和Map的区别4.ListList——ArrayList&#xff08;&#xff01;&#xff01;实用…

武汉星起航:跨境电商获各大企业鼎力支持,共筑繁荣生态

随着全球化和数字化的深入发展&#xff0c;跨境电商行业逐渐成为连接国内外市场的重要桥梁。在这一进程中&#xff0c;各大企业纷纷加大对跨境电商行业的支持力度&#xff0c;通过投资、合作与创新&#xff0c;共同推动行业的繁荣与发展。武汉星起航将探讨各大企业对跨境电商行…

图片标注编辑平台搭建系列教程(6)——fabric渲染原理

原理 fabric的渲染步骤大致如下&#xff1a; 渲染前都设置背景图然后调用ctx.save()&#xff0c;存储画布的绘制状态参数然后调用每个object自身的渲染方法最后调用ctx.restore()&#xff0c;恢复画布的保存状态后处理&#xff0c;例如控制框的渲染等 值得注意的是&#xff0…

Verilog语法之case语句学习

case分支语句是一种实现多路分支控制的分支语句。与使用if-else条件分支语句相比&#xff0c;采用case分支语句来实现多路控制会变得更加的方便直观。 case分支语句通常用于对微处理器指令译码功能的描述以及对有限状态机的描述。Case分支语句有“case”、“casez”、“casex”…

使用pdf表单域填充pdf内容

需要引用如下包 <dependency><groupId>com.itextpdf</groupId><artifactId>itext-core</artifactId><version>8.0.3</version><type>pom</type></dependency>1、预先准备一个pdf模板&#xff0c;并在指定位置添加…

C/C++语言实现简易通讯录 [含文件操作,循环双链表]

文章目录 C/C语言实现简易通讯录概要基本功能运行截图展示主要代码展示 &#x1f396; 博主的CSDN主页&#xff1a;Ryan.Alaskan Malamute &#x1f4dc; 博主的代码仓库主页 [ Gitee ]&#xff1a;ryanala [GitHub]&#xff1a; Ryan-Ala C/C语言实现简易通讯录 ⚠⚠⚠ …

Android 开发投屏软件

一、背景 作为Android开发总会有给他人share自己APP情况&#xff0c;一般在线会议投屏&#xff0c;总是需要在手机上安装对应会议软件特别麻烦~ 二、投屏 Android Studio已经自带了投屏能力&#xff0c;可以在电脑端直接控制手机&#xff0c;同步起来非常方便简单 打开步骤 …

【Kubernetes】K8s 中的 Pod 驱逐

K8s 中的 Pod 驱逐 1.Pod 被驱逐的原因&#xff1a;抢占和节点压力2.抢占式驱逐2.1 Pod 调度2.1.1 过滤2.1.2 计分 2.2 Pod 优先级2.3 优先级示例 3.节点压力驱逐3.1 服务质量等级3.1.1 Guaranteed3.1.2 Burstable3.1.3 BestEffort 4.其他类型的驱逐4.1 API 发起的驱逐&#xf…

[机器学习]练习-KNN算法

1&#xff0e;&#x1d458;近邻法是基本且简单的分类与回归方法。&#x1d458;近邻法的基本做法是&#xff1a;对给定的训练实例点和输入实例点&#xff0c;首先确定输入实例点的&#x1d458;个最近邻训练实例点&#xff0c;然后利用这&#x1d458;个训练实例点的类的多数来…

代码随想录算法训练营三刷 day38 | 动态规划之 509. 斐波那契数 70. 爬楼梯 746. 使用最小花费爬楼梯

三刷day38 509. 斐波那契数1 确定dp数组以及下标的含义2 确定递推公式3 dp数组如何初始化4 确定遍历顺序5 举例推导dp数组 70. 爬楼梯1 确定dp数组以及下标的含义2 确定递推公式3 dp数组如何初始化4 确定遍历顺序5 举例推导dp数组 746. 使用最小花费爬楼梯1 确定dp数组以及下标…

Disruptor

前言 大家好&#xff0c;我是jiantaoyab&#xff0c;这是我作为学习笔记总结应用篇最后一篇&#xff0c;本章大量的参考了别的博主的文章。 我们今天一起来看一个开源项目 Disruptor。看看我们怎么利用 CPU 和高速缓存的硬件特性&#xff0c;来设计一个对于性能有极限追求的系…

vlan间单臂路由

【项目实践4】 --vlan间单臂路由 一、实验背景 实验的目的是在一个有限的网络环境中实现VLAN间的通信。网络环境包括两个交换机和一个路由器&#xff0c;交换机之间通过Trunk链路相连&#xff0c;路由器则连接到这两个交换机的Trunk端口上。 二、案例分析 在网络工程中&#…

python批量转化pdf图片为jpg图片

1.把pdf图片批量转为jpg&#xff1b;需要注意的是&#xff0c;需要先安装poppler这个软件&#xff0c;具体安装教程放在下面代码中了 2.代码 #poppler安装教程参考&#xff1a;https://blog.csdn.net/wy01415/article/details/110257130 #windows上poppler下载链接&#xff1a…

只出现一次的数字 II

题目链接 只出现一次的数字 II 题目描述 注意点 nums中&#xff0c;除某个元素仅出现一次外&#xff0c;其余每个元素都恰出现三次设计并实现线性时间复杂度的算法且使用常数级空间来解决此问题 解答思路 本题与只出现一次的数字的数字类似&#xff0c;区别是重复的数字会…

游戏领域AI智能视频剪辑解决方案

游戏行业作为文化创意产业的重要组成部分&#xff0c;其发展和创新速度令人瞩目。然而&#xff0c;随着游戏内容的日益丰富和直播文化的兴起&#xff0c;传统的视频剪辑方式已难以满足玩家和观众日益增长的需求。美摄科技&#xff0c;凭借其在AI智能视频剪辑领域的深厚积累和创…

手写SpringBoot(三)之自动配置

系列文章目录 手写SpringBoot&#xff08;一&#xff09;之简易版SpringBoot 手写SpringBoot&#xff08;二&#xff09;之动态切换Servlet容器 手写SpringBoot&#xff08;三&#xff09;之自动配置 手写SpringBoot&#xff08;四&#xff09;之bean动态加载 手写SpringBoot…

内网穿透时报错【Bad Request This combination of host and port requires TLS.】的原因

目录 1.内网直接https访问&#xff08;可以正常访问&#xff09; 程序配置的证书 2.内网穿透后,通过外网访问 3.原因 4.内网非https的Web应用&#xff0c;使用https后&#xff0c;也变成了https访问 5.题外话 感觉自己的web应用配置了https&#xff0c;反而影响了内网穿…