Vue.js关于父子组件数据双向绑定的思考以及四种实现方案
为什么要做父子组件数据双向绑定❓
一般来说父子组件的通信都是单向的,父组件使用props
向子组件传递数据,或者是子组件使用emit
向父组件传递数据,但是单向数据流使用起来是不方便的,下面我拿一个常见的场景举个例子,了解我们为什么要做父子组件的双向绑定。
- 子组件里包含表格+分页的功能,父组件内执行表格数据与分页数据的获取,并向子组件传递
- 子组件通过分页操作更新分页数据,此刻父组件的分页数据因为子组件的修改而更改,再通过
emit
触发父组件的数据获取事件,传递分页数据。
反过来思考一下,如果我们使用emit
直接传递分页数据给父组件也是可以的,但是我们为什么要用双向绑定呢?我觉得将一些通用性的数据共享出来,比如一些请求参数,使用双向绑定会比单向数据流更加得清晰,而且我们不用考虑数据更新后使用单向数据流传递引起的心智负担。
子组件直接修改props不就可以了吗?💢
我们可以使用props
来实现父组件向子组件传值,那么我们直接在子组件修改一下props不就好了吗?但是当我们想要在子组件对父组件传来的prop进行修改时,就会向我们抛出警告它是只读的,究其原因也很好理解。
官方文档给出的解释为:所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
双向绑定的开胃菜🍨
我们都知道双向绑定指令v-model
,比如
<input v-model="content" />
其实他是v-bind
与v-on
指令的语法糖,在模板编译后,v-model进行了等价展开
<input
:value="content"
@input="content = $event.target.value"
/>
解析一下展开的两行
- 使用
v-bind
响应式绑定input
的value
属性值 - 使用
@input
事件,来更新content
值 - 更新后的
content
响应式地更新给value
在了解v-model
的本质后,我们再来看看我们如何在组件上进行v-model
,实现父子组件数据的双向绑定
组件上的v-model😶🌫️
官方文档给的实例很全面,传送门:组件 v-model | Vue.js (vuejs.org),我在这里以自己的想法解析一下
<!--父组件-->
<CustomInput v-model="message" />
<!--子组件-->
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue']
}
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
这里的父组件中使用v-model
会被解析为v-bind
的形式,作为prop
传入子组件,而子组件自定义了update:modelValue
事件来更新modelValue
的值,子组件的@input
触发自定义事件,更新modelValue
值
结合计算属性
在上面的基础上,我们可以更改自定义事件触发机制,结合计算属性,也就是通过set`来触发自定义事件
<!--子组件-->
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
computed: {
value: {
get() {
return this.modelValue
},
set(value) {
this.$emit('update:modelValue', value)
}
}
}
}
</script>
<template>
<input v-model="value" />
</template>
我们将父组件传入的值通过计算属性进行了扩展,和之前的写法相比,将主要逻辑给到了计算属性上
- 在子组件内定义计算属性
value
,在input
进行v-model的绑定 - 父组件将值传递给子组件,子组件通过计算属性的get返回该值
- 当input输入后,值发生改变,触发set后,执行自定义事件,更新值
多个v-model
当出现了要双向绑定多个值的情况下,只需要做出一点点改变
<!--父组件-->
<CustomInput
v-model:name="name"
v-model:age="age"
/>
<!--子组件-->
<script>
export default {
props: ['name','age'],
emits: ['update:name','update:age']
}
</script>
<template>
<input
:value="name"
@input="$emit('update:name', $event.target.value)"
/>
</template>
父组件中在v-model后加上不同的prop
标识符,子组件中增加对不同prop
对应的自定义事件即可
自定义model
还有最后一种实现父子组件数据双向绑定的方法
<!--父组件-->
父组件的{{ count }}
<button @click="add">+</button>
<ColumnTable v-model="count"></ColumnTable>
<!--子组件-->
子组件的count:{{ count }}
<button @click="add">+</button>
<script>
export default {
name: 'ColumnTable',
model: {
prop: 'count',
event: 'change-count'
},
props: {
count: {
type: Number,
default: 0
}
},
methods: {
add() {
this.$emit('change-count', this.count + 1)
},
}
}
</script>
我们在子组件内自定义model,其有prop
与event
属性,而我们在子组件内触发在model定义好的event
,通过this
修改prop
后,父子组件的count
值都发生了改变,说明实现了双向绑定。
通过引用类型特性绕过props
他不让我修改props
的值,但他也没说我不能改prop
的属性的值啊,因为引用类型的存储的是地址,所以我们保持地址不变,也就是prop的值不改变,去改变引用类型里面的值,这样就可以瞒天过海,实现双向绑定
- 我们在父组件内声明了一个引用类型的响应式数据
dataObj
,通过prop
传给子组件 - 在子组件内,我们在
props
定义时嵌套好dataObj
与value
的关系 - 然后通过
this
改变dataObj
的value
值,父子组件的dataValue
都发生了改变
转载自:https://juejin.cn/post/7204735262961188921