Vue(五):自定义双向数据绑定

104 阅读2分钟

Vue2 中使用 v-model 实现自定义数据双向绑定。

默认情况

默认情况下,v-model 会把父组件的数据(比如 v-model="count" 中的 count)绑定到子组件的 value 属性上;

子组件通过 $emit 触发 input 事件时,Vue 会将事件的参数传递回父组件,从而更新父组件的数据。

父组件:

<!-- 父组件 -->
<template>
  <div>
    <Children v-model="count"></Children>
    <p>----------</p>
    <p>
        父: {{ count }}
        <button @click="handleAdd">父按钮</button>
    </p>
  </div>
</template>

<script>
import Children from './Children.vue'

export default {
  components: { Children },
  data() {
    return {
      count: 1
    }
  },
  mounted() {
    handleAdd() {
        this.count++;
    }
  }
}
</script>

子组件:

<!-- 子组件 -->
<template>
  <div>
    子:
    <input :value="value" @input="handleInput">
    <button @click="handleAdd">子按钮</button>
  </div>
</template>
 
<script>
export default {
  props: {
    value: {
      type: Number,
      default: 2
    },
  },
  methods: {
    handleInput(e) {
        console.log(typeof e.target.value) // string
        this.$emit('input', Number(e.target.value));
    },
    handleAdd() {
        this.value++; // 报错
    }
  },
  watch: {
      value: {
          handler(newVal, oldVal) {
              console.log("newVal:", newVal)
              console.log("oldVal:", oldVal)
          },
          immediate: true
      }
  }
}
</script>

此时在 input 框中输入数字、或点击父组件的按钮,都会实现父子组件的双向数据绑定.

注意点:

  • 父组件中 count 的默认值为 1,子组件中 count 的默认值为 2,实际显示的默认值是 1。

  • 页面初始化时 watch 中打印的内容为:newVal: 1,oldValu: undefined

  • e.target.value 的数据类型是 string


父组件中改变双向绑定的数据很简单,例如在上例中,点击父按钮可以轻松实现,但是点击子按钮就会报错:

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop’s value. Prop being mutated: "value"

避免直接修改 prop,因为每当父组件重新渲染时,该属性的值都会被覆盖。应根据该属性的值使用 data 或计算属性 computed。正在被修改的属性:"value"

下面给出正确示例,父组件代码不变,只修改子组件代码:

方式一:使用 data

<!-- 子组件 -->
<template>
  <div>
    子:
    <input :value="value" @input="handleInput">
    <button @click="handleAdd">子按钮</button>
  </div>
</template>
 
<script>
export default {
  props: {
    value: {
      type: Number,
      default: 2
    },
  },
  data() {
      localValue: this.value
  },
  methods: {
    handleInput(e) {
        console.log(typeof e.target.value) // string
        this.$emit('input', Number( e.target.value));
    },
    handleAdd() {
        this.localValue++;
        this.$emit('input', this.localValue);
    }
  }
  // watch 内容省略
}
</script>

方式二:使用 computed

<!-- 子组件 -->
<template>
  <div>
    子:
    <input :value="value" @input="handleInput">
    <button @click="handleAdd">子按钮</button>
  </div>
</template>
 
<script>
export default {
  props: {
    value: {
      type: Number,
      default: 2
    },
  },
  computed: {
      localValue: {
          get() {
              return this.value
          },
          set(newVal) {
              this.$emit('input', newVal)
          }
      }
  },
  methods: {
    handleInput(e) {
        console.log(typeof e.target.value) // string
        this.$emit('input', Number( e.target.value));
    },
    handleAdd() {
        this.localValue++;
    }
  }
  // watch 内容省略
}
</script>

两种方式的本质都是通过 $emit 触发 input 事件,将更新后的值传递给父组件。


如果子组件中不是 input,而是 select 呢?其实本质是一样的,都是通过 $emit 触发 input 事件,将更新后的值传递给父组件。

下面给出示例,父组件代码不变,只修改子组件代码:

<!-- 子组件 -->
<template>
  <div>
    子:
    <el-select v-model="localValue">
        <el-option v-for="item in 10" 
            :key="item"
            :label="`选项`${item}"
            :value="item"
        >
        </el-option>
    </el-select>
    <button @click="handleAdd">子按钮</button>
  </div>
</template>
 
<script>
export default {
  props: {
    value: {
      type: Number,
      default: 2
    },
  },
  computed: {
      localValue: {
          get() {
              return this.value
          },
          set(newVal) {
              this.$emit('input', newVal)
          }
      }
  },
  data() {
      // localValue: this.value
      /*
      data 中也可以,但是需要手动捕获 localValue 的变化事件,
      在变化的时候调用 this.$emit('input', this.localValue) 方法,
      不如 computed 方便。
      */
  },
  methods: {
    handleAdd() {
        this.localValue++;
    }
  }
  // watch 内容省略
}
</script>

无论换成什么,只要在变化的时候使用 this.$emit 触发默认的 input 事件就可以。

自定义属性和事件

在子组件中,你可以通过 model 选项来指定一个不同的 prop 名称用于接收值,以及一个不同的 event 名称用于触发更新。

父组件代码还是不变:

<!-- 父组件 -->
<template>
  <div>
    <Children v-model="count"></Children>
    <p>----------</p>
    <p>
        父: {{ count }}
        <button @click="handleAdd">父按钮</button>
    </p>
  </div>
</template>

<script>
import Children from './Children.vue'

export default {
  components: { Children },
  data() {
    return {
      count: 1
    }
  },
  mounted() {
    handleAdd() {
        this.count++;
    }
  }
}
</script>

子组件把属性定义为 aaProp,事件定义为 bbEven:

<!-- 子组件 -->
<template>
  <div>
    子:
    <input :value="aaProp" @input="handleInput">
  </div>
</template>
 
<script>
export default {
  model: {
    prop: "aaProp",
    event: "bbEven"
  }
  props: {
    aaProp: {
      type: Number,
      default: 2
    },
  },
  methods: {
    handleInput(e) {
        this.$emit('bbEven', Number(e.target.value));
    },
  }
}
</script>