likes
comments
collection
share

Vue.js关于父子组件数据双向绑定的思考以及四种实现方案

作者站长头像
站长
· 阅读数 14

为什么要做父子组件数据双向绑定❓

一般来说父子组件的通信都是单向的,父组件使用props向子组件传递数据,或者是子组件使用emit向父组件传递数据,但是单向数据流使用起来是不方便的,下面我拿一个常见的场景举个例子,了解我们为什么要做父子组件的双向绑定。

Vue.js关于父子组件数据双向绑定的思考以及四种实现方案

  • 子组件里包含表格+分页的功能,父组件内执行表格数据与分页数据的获取,并向子组件传递
  • 子组件通过分页操作更新分页数据,此刻父组件的分页数据因为子组件的修改而更改,再通过emit触发父组件的数据获取事件,传递分页数据。

反过来思考一下,如果我们使用emit直接传递分页数据给父组件也是可以的,但是我们为什么要用双向绑定呢?我觉得将一些通用性的数据共享出来,比如一些请求参数,使用双向绑定会比单向数据流更加得清晰,而且我们不用考虑数据更新后使用单向数据流传递引起的心智负担。

子组件直接修改props不就可以了吗?💢

我们可以使用props来实现父组件向子组件传值,那么我们直接在子组件修改一下props不就好了吗?但是当我们想要在子组件对父组件传来的prop进行修改时,就会向我们抛出警告它是只读的,究其原因也很好理解。

官方文档给出的解释为:所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。

双向绑定的开胃菜🍨

我们都知道双向绑定指令v-model,比如

<input v-model="content" />

其实他是v-bindv-on指令的语法糖,在模板编译后,v-model进行了等价展开

<input
  :value="content"
  @input="content = $event.target.value"
/>

解析一下展开的两行

  • 使用v-bind响应式绑定inputvalue属性值
  • 使用@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,其有propevent属性,而我们在子组件内触发在model定义好的event,通过this修改prop后,父子组件的count值都发生了改变,说明实现了双向绑定。

通过引用类型特性绕过props

他不让我修改props的值,但他也没说我不能改prop的属性的值啊,因为引用类型的存储的是地址,所以我们保持地址不变,也就是prop的值不改变,去改变引用类型里面的值,这样就可以瞒天过海,实现双向绑定

Vue.js关于父子组件数据双向绑定的思考以及四种实现方案

  • 我们在父组件内声明了一个引用类型的响应式数据dataObj,通过prop传给子组件
  • 在子组件内,我们在props定义时嵌套好dataObjvalue的关系
  • 然后通过this改变dataObjvalue值,父子组件的dataValue都发生了改变