vue中自定义组件v-model的用法

1,954 阅读3分钟

前言

面试官: 你讲了你封装了不少组件,那自定义组件中有用过v-model吗,知道他的实现过程吗,那v-model的修饰符,比如常用的input输入框中v-model.trim是如何实现的吗。

我: 用过(其实不怎么用,只知道个大概,平常封装组件直接传值props这样的)就是相当于在子组件里面定义value值,它会有个默认的emit事件input进行回调来对父组件数据进行更新。自定义的修饰符没用过,如果需要处理数据,一般会采用事件来对数据再进一层处理

面试官: 那如果我是单选,多选种类的怎么更新呢

我: 应该差不多吧

面试官: 也是value吗,如果是的话,单选多选使用时不会冲突吗

我: 那,这,不知道 然后就一阵沉默结束

面试官: 那,这,一阵沉默后说你等通知吧

vue2中自定义组件 v-model 的实现

我们来看下官网的说法 image.png

  1. 我们根据 api 来测试一下,在子组件里面使用 input 输入框定义 value ,用默认的回调方式更新
**父组件:**
<template>
  <div class="v-model">
    父组件的值 <span>{{ inputValue }}</span>
    <myChildren v-model="value"></myChildren>
  </div>
</template>
<script>
import myChildren from "./children.vue"
  export default{
    name:'Parent-level',
    components:{
      myChildren
    },
    data(){
      return{
        value:'父组件初始值'
      }
    }
  }
</script>

**子组件:**
<template>
    <div class="v-model-children">
      子组件的值
      <input type="text" :value="value" @input="(e)=>{$emit('input',e.target.value)}" class="input">
    </div>
</template>
<script>
  export default{
    name:'children-input',
    props:{
        value:{
            type:String,
            default:''
        }
    },
  }
</script>

那如果我们改下 props 中接收的 value 为其他字段呢,还能父子数据同步吗。结果是虽然子组件更新父组件能更新,但是父组件的初始值子组件是拿不到的。
那我们换个方式来进行子组件初始值更新,在data里面定义一个数据,监听父组件传过来的值,有值更新后对data里面定义的值进行更新

*子组件*
<template>
    <div class="v-model-children">
      子组件的值
      <input type="text" :value="inputValue" @input="(e)=>{$emit('input',e.target.value)}" class="input">
    </div>
</template>
  
<script>
  export default{
    name:'children-input',
    props:{
        value:{
            type:String,
            default:''
        }
    },
    data(){
        return{
            inputValue:''
        }
    },
    watch:{
        value: {
            handler(newval) {
                this.inputValue=newval
            },
            immediate: true
        }
    },
  }
</script>

结果是可行,但多此一举,多写了几行代码,少摸了一会鱼。

  1. 我们来看下其他类型的 model 绑定
*父组件*
<template>
  <div class="v-model">
    <span>父组件的值{{ value }}</span>
    <myChildren v-model="value"></myChildren>
  </div>
</template>
<script>
import myChildren from "./index-children.vue"
  export default{
    name:'Parent-level',
    components:{
      myChildren
    },
    data(){
      return{
        value:'3',
      }
    },
  }
</script>

*子组件*
<template>
    <div class="v-model-children">
      子组件的值
      <select name="fruit"  :checked="checked" v-on:change="$emit('change', $event.target.value)">
        <!-- <option value="apple" >apple</option>
        <option value="pear" >pear</option>
        <option value="watermelon" :selected="checked==='watermelon'">watermelon</option>
        <option value="banana" >banana</option> -->
        <option v-for="item in options" :value="item.id" :selected="checked===item.id">{{ item.label }}</option>
      </select>
    </div>
</template>
  
<script>
  export default{
    name:'children-input',
    model:{
       prop:'checked',
       event:'change'
    },
    props:{
      checked:{
          type:String,
          default:''
        },
    },
    data(){
        return{
           options:[
            {
                label:'apple',
                id:'1'
            },
            {
                label:'pear',
                id:'2'
            },
            {
                label:'watermelon',
                id:'3'
            },
            {
                label:'banana',
                id:'4'
            },
           ]
        }
    }
  }
</script>

我们把子组件里面的 model 设置注释掉,则父组件初始值就不会同步更新到子组件。假如想使用自定义的 prop 和事件,我们把这个 model 设置为我们自己需要的,就可以实现自定义组件的 v-model 。

.sync修饰符
假如我们一个 Model 弹窗,希望实现一个数据双向绑定的效果,我们也可以使用 .sync 修饰符来处理。可以这样来理解,.sync 起到父组件数据实时更新到子组件的作用,但是需要搭配特定的 update 回调才起作用。

*父组件*
<template>
  <div class="v-model">
    <span>父组件</span>
    <button @click="()=>visible=true">显示子组件</button>
    <myChildren :visible.sync="visible"></myChildren>
  </div>
</template>

<script>
import myChildren from "./index-children.vue"
  export default{
    name:'Parent-level',
    components:{
      myChildren
    },
    data(){
      return{
        visible:false,
      }
    }
  }
</script>

*子组件*
<template>
    <div class="v-model-children" v-if="visible">
      显示子组件
      <button @click="()=>$emit('update:visible',false)"> 移除子组件</button>
    </div>
</template>
  
<script>
  export default{
    name:'children-input',
    props:{
      visible:{
          type:Boolean,
          default:false
        },
    }
  }
</script>

总结一下: 总得来说相比于我们平常封装的组件,少了一个子组件中 props 的重新取值,和父组件的更新回调。但是它也是有不足的地方,一个自定义组件只有一个v-model 。.sync 的数据双向绑定需要搭配固定的 update 回调才能进行实时更新

vue3中自定义组件 v-model 的实现

我们先来看下官网的用法
默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件。我们可以通过向 v-model 传递参数来修改这些名称:

我们先用代码实现一下

*父组件*
<template>
  <div class="wrapper">
    父组件:{{value  }}
      <HelloWorld v-model="value"/>
      等价于 <!-- <HelloWorld v-model:model-value="value"/> -->
    </div>
</template>
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
import { ref } from 'vue';
const value=ref('父级xxx')
</script>

*子组件*
<template>
  <div class="greetings">
    <span>子组件:   </span>{{ modelValue }}
    <button @click="()=>$emit('update:modelValue','子级xxx')">更新子组件</button>
  </div>
</template>
<script setup lang="ts">
defineProps({
  modelValue:{
    type:String,
    default:''
  }
 })
 defineEmits(['update:modelValue'])
</script>

有没有很眼熟,和 vue2 中 .sync 方式差不多,都是通过 update 进行回调更新。算是 vue2 中 v-model.sync 实现方式的结合体。

那如果我们想不使用modeValue ,使用自定义的prop呢?
我们把子组件中 modelValue 都替换为 myModel ,先不改动父组件测试一下,发现父组件无法更新了。我们来修改一下父组件的绑定属性改为 v-model:my-model="value" 就可以实现父子组件同步更新了 vue3可以定义多个model

*父组件*
 <template>
  <div class="wrapper">
    父组件:{{value  }}
    <br>
    父组件2:{{value2  }}
      <HelloWorld v-model:my-model="value" v-model:my-model2="value2"/>
    </div>
</template>
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
import { ref } from 'vue';
const value=ref('父级xxx')
const value2=ref('父级xxx2')
</script>

*子组件*
<template>
  <div class="greetings">
    <div>
      <span>子组件:   </span>{{ myModel }}
       <button @click="()=>$emit('update:myModel','子级xxx')">更新子组件</button>
    </div>
    <div>
      <span>子组件2:   </span>{{ myModel2 }}
       <button @click="()=>$emit('update:myModel2','子级xxx2')">更新子组件2</button>
    </div>
  </div>
</template>
<script setup lang="ts">
defineProps({
  myModel:{
    type:String,
    default:''
  },
  myModel2:{
    type:String,
    default:''
  }
 })
 defineEmits(['update:myModel','update:myModel2'])
</script>

看起来和定义一个 v-model 没什么区别,就是就是多定义了一组父子级数据而已

那vue3中v-model修饰符是如何使用的呢?

在 3.x 中,添加到组件 v-model 的修饰符将通过 modelModifiers prop 提供给组件

意思就是如果我们使用修饰符,需要在定义 prop 的时候进行修饰符的定义,我们用代码来实现一下

*父组件*
<template>
  <div class="wrapper">
    父组件:{{value  }}
    <br>
      <HelloWorld v-model:my-model.capitalize="value"/>
    </div>
</template>
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
import { ref } from 'vue';
const value=ref('fujishjux')
</script>

*子组件*
<template>
  <div class="greetings">
    <span>子组件:   </span>{{ myModel }}
      <input type="text" 
      :value="myModel"
      @input="inputChange">
  </div>
</template>
<script setup lang="ts">
const props:any=defineProps({
  myModel:{
    type:String,
    default:'',
  },
  myModelModifiers:{
    default: () => ({})
  }
 })
 const emit=defineEmits(['update:myModel'])
 const inputChange=(e:any)=>{
  let value = e.target.value
  // console.log(props.myModelModifiers.capitalize)
  if(props.myModelModifiers.capitalize){
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
    emit('update:myModel',value)
 }
</script>

我们在 prop 后进行了一个修饰符操作的定义,当父组件添加了修饰符,在子组件中就能知道需要对数据进行处理!
有个点需要注意一下,就是修饰符的定义就是我们定义的 prop + Modifiers,否则就会无效

结语

到这里自定义 v-model 已经聊完了,虽然这是一个很小的点,但是对我们开发中也很有帮助。我是南岸月明致力于用最简洁的话聊清楚一个个知识点,共同进步是我目标!