浅谈vue、react封装复杂组件的数据流
前言
这两天搞了一个7年前的vue2的前端项目,给它开发了一个复杂的搜索组件,就是腾讯云上常见的搜索框,就是表面上看起来是一个搜索框, 但是它支持在里面输入多种搜索条件,有的条件是单选,有的条件是多选,条件都是配置的,有些是手动输入的字符串,有些是要通过option来提供选择, 这个组件其实是很复杂的,然后在开发的过程中也遇到了一些数据流的问题,就回想起这些年不论是vue2、还是vue3、以及react中每次封装组件都或多或少会 遇到一些因为数据流带来的困扰,所以今天想浅谈一下。
这次我遇到的问题
这次vue2中开发的组件最终就类似个样子,获得焦点时弹出选项,选择选项后会根据选项是自定义组件还是普通字符串,然后渲染不同的录入方式,支持修改、删除, 支持收起。
然后我就遇到了一个问题,内部的关键词组(灰色背景,带删除按钮)的部分是一个单独的子组件,但是我在使用的时候犯了一个错误,我使用了他的值来当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个关键原因被我凑齐了
- 使用了引用传递。
- 忽略了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事件来同步子组件内部的修改到当前数据中的,这就是单向数据流。
单向数据流更麻烦,但是对于数据的变化是更加可控的。
子组件封装的技巧
尤其是在封装复杂组件时,为了不让我们的组件是双向数据流,要斩断双向数据流,但是又要实现父组件可以修改值,子组件响应父组件的修改,重新处理数据并渲染。子组件修改值后及时 同步到父组件中。我们通常只需要这样做。
- 子组件不能直接修改父组件传入的值,深拷贝值对象(数组)
- 子组件使用特定的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呢?