Skip to Content
全部文章前端大杂烩浅谈vue、react封装复杂组件的数据流

浅谈vue、react封装复杂组件的数据流

前言

这两天搞了一个7年前的vue2的前端项目,给它开发了一个复杂的搜索组件,就是腾讯云上常见的搜索框,就是表面上看起来是一个搜索框, 但是它支持在里面输入多种搜索条件,有的条件是单选,有的条件是多选,条件都是配置的,有些是手动输入的字符串,有些是要通过option来提供选择, 这个组件其实是很复杂的,然后在开发的过程中也遇到了一些数据流的问题,就回想起这些年不论是vue2、还是vue3、以及react中每次封装组件都或多或少会 遇到一些因为数据流带来的困扰,所以今天想浅谈一下。

这次我遇到的问题

这次vue2中开发的组件最终就类似个样子,获得焦点时弹出选项,选择选项后会根据选项是自定义组件还是普通字符串,然后渲染不同的录入方式,支持修改、删除, 支持收起。

1

1

1

然后我就遇到了一个问题,内部的关键词组(灰色背景,带删除按钮)的部分是一个单独的子组件,但是我在使用的时候犯了一个错误,我使用了他的值来当key, 并且传入的数据是一个对象,子组件内部会把对象内的value赋值到一个内部数据中,当添加、删除选项时并不会emit change事件,而要等到点击确认按钮才会emit,

因为我这里的每个关键词的value都是有一个唯一的前缀,并且一个条件录入后,这个条件就不会再出现在popup中供选择了,所以理论上是没问题的。

<KeyWords v-for="(keyword, index) in keywords" :data="keyword" :overflowWrap="containerIsFocus || isHoverHold" @change="onEdit($event, keyword)" @remove="removeKeyword(index)" :key="JSON.stringify(keyword.value)" />

子组件内部接收value

mounted() { this.currValue = this.data.value; },

但是我在开发的过程中发现,只要我修改某个条件,给他添加(this.currValue.push(...))、删除(this.currValue.splice(...))可选项时keyword组件内的popup就会消失,后来我才想起来这是子组件重新渲染了,而为什么内部修改了 this.currValue会导致外部传入data发生变化的原因? 就是因为这里赋值时使用出现了问题,在js中对象是引用传递,而对于复杂组件来说,他的值是对象(数组)的情况 非常常见。这就导致了虽然我修改的是内部的currValue,实际父组件中的data已经被修改到了,data的变化进而导致外面循环:key发生变化,然后卸载了这个组件,并重新 初始化。

问题原因是找到了,这里问题导致的2个关键原因被我凑齐了

  1. 使用了引用传递。
  2. 忽略了key的变化导致组件重新渲染。

然后我就回忆起在我封装过的组件中,不管是vue2,还是vue3以及react都是经常会需要封装一个复杂组件,数据双向流动的情况,就是外部可以修改值,触发子组件 更新渲染,子组件内部也可以修改值触发外部更新值。这种双向绑定非常常见,但是双向绑定非常非常容易导致数据流混乱,遇到奇奇怪怪的问题,难以定位数据变动的 原因和具体位置。

数据流

什么是数据流?数据流就是数据的流向,我们从vue和react来看会发现他们的数据绑定是很不一样的。

比方说在vue中常见的v-model,他就是一个典型的双向绑定

假如有一个vue组件,内部引用了一个子组件(这里使用input作为子组件,不需要自己开发逻辑)

// 模板 <input v-model="data"> // vue2 data(){ return { data:'' } } // vue3 const data = ref('')

这里的双向绑定会让你很方便, 不论你再父组件中修改data的值,还是在子组件(input)中输入,还是在父组件中直接修改data的值,子组件输入框内和父组件中的 data是同步的,这就是双向绑定,看起来很方便哈。

再来,假如是react呢?

// 模板 <input value={data} onChange={onChange} /> // react const [data, setData] = useState('') const onChange = (e) => { setData(e.target.value) }

这里可以看到react都是要写一个onChange事件来同步子组件内部的修改到当前数据中的,这就是单向数据流。

单向数据流更麻烦,但是对于数据的变化是更加可控的。

子组件封装的技巧

尤其是在封装复杂组件时,为了不让我们的组件是双向数据流,要斩断双向数据流,但是又要实现父组件可以修改值,子组件响应父组件的修改,重新处理数据并渲染。子组件修改值后及时 同步到父组件中。我们通常只需要这样做。

  1. 子组件不能直接修改父组件传入的值,深拷贝值对象(数组)
  2. 子组件使用特定的change事件

不论是react还是vue,在组建内部的mounted、watch或者useEffect中,对外部传入值的修改做好初始化和监听处理,及时响应外部的变化。

vue2中子组件

props:{ data:Object }, data(){ return { innerData:null } }, mounted() { this.innerData = cloneDeep(this.data);// 深度拷贝复制一个对象出来 }, watch:{ data:{ handler(newVal) { this.innerData = cloneDeep(newVal);// 深度拷贝复制一个对象出来 }, deep: true } }, methods:{ // 在需要的时候调用change事件将内部数据同步出去 onChange(){ this.$emit('change', this.innerData); } }

vue3中子组件

const props = defineProps({ data:Object }) const emit = defineEmits(['change']) const innerData = ref(null) onMounted(() => { innerData.value = cloneDeep(props.data);// 深度拷贝复制一个对象出来 }) watch(() => props.data, (val) => { innerData.value = cloneDeep(val);// 深度拷贝复制一个对象出来 }, { deep: true }) // 在需要的时候调用change事件将内部数据同步出去 const onChange = () => { emit('change', innerData.value); }

react中子组件

const { data,change } = props; const [innerData, setInnerData] = useState(null); useEffect(() => { setInnerData(cloneDeep(data));// 深度拷贝复制一个对象出来 }, [JSON.stringify(data)]); // 在需要的时候调用change事件将内部数据同步出去 const onChange = () => { change(innerData); }

这样就很大程度上阻断了双向数据流,实现了数据的单向流动。你的自定义组件会更加稳定,不会出现莫名其妙的变化和难以追踪的问题。

谈点题外话

1.就像上面的例子中,v-for的key的作用也是很重要的,轻则影响性能,重则会导致组件的重新渲染,带来一些意外的效果。 2.还有vue2尽早别用了,升级到vue3更好,vue3可以更加聚合逻辑,把相关联的逻辑放到一起,vue2这种methods全部在一堆,data全部在一堆的这种配置式的代码,对于大型复杂组件的开发维护都是带来一些 额外的工作。 3.用typescript开发吧,复杂应用和组件虽然开发工作量上升,但是维护性好太多。 4.用react开发吧,虽然各种hook有一些副作用,但是jsx香得不行,vue中使用jsx普遍不是太多,既然都要在vue中写jsx了,为什么不考虑一下react呢?

最后编辑于

hi