【Vue】组件的v-model,以及二次封装组件

477 阅读3分钟

Vue2

<son v-model='message'></son>

message:123
  1. 给子组件绑定v-model

  2. 子组件默认通过value接收message(需写props接收value),通过this.$emit('input','456')修改

    props:['value'],//默认接收v-model的值为value,此行可省略
    methods:{
            handleClick(){
            this.$emit('input',456)
        }
    }
  1. 子组件也可自定义model改变props和事件名称,修改时通过this.$emit(‘model绑定的event名称’,'修改的值')
    model:{
        prop:myValue,
        event:'myChange'
    },
    props:['myValue'],
    methods:{
        handleClick(){
            this.$emit('myChange',456) 
            //此时父组件data中的message已被改为456
        }
    }

3.如不通过model自定义,可通过
完善:

  1. 此时组件的V-model 父 -> 子已实现,接下来需要实现 子 -> 父,即 子组件中修改数据同步父组件数据
    <input v-model="childValue">我是子组件中的标签<input/>
    computed:{
        childValue:{
            get(){
                return this.myValue
                //通过get set劫持,使用childValue时拿的是父v-model的值,
            },
            set(val){
                this.$emit('myChange',val)
                // 对数据进行修改时触发model事件,修改父v-model的值,此时组件的v-model就实现了。
            }
        }
    }

vue3

Vue3和vue2稍有不同,Vue2默认接收是value,Vue3(setup语法)中是modelValue,vue3中不需要自定义model,固定语法 this.$emit('update:model-value',val)

    <Son v-model:myValue="message">
    // 子组件 set劫持时,this.$emit('update:modelValue',val)

可通过给v-model传参更改默认modelValue值,同时实现传递多个v-model

<son v-model:name="" v-model:age.modifiers=""></son>
    此时son组件可以接收props,name和age
<son>组件
props:{
    name:String,
    age:String
},
methods:{
    setName(){
        this.$emit('update:name',123)
    },
    setAge(){
        this.$emit('update:age',123)
    }
}

扩展:可传递v-model修饰符,可根据修饰符对v-model值进行处理。 加了 .modifiers 之后可以接收一个props为modelModifiers对象,通过this.modelModifiers.modifiers 是否为true做一些逻辑处理(如果加了修饰符为true)

3.完善一下 vue3二次封装组件

1.组件的v-model

1. 方法1
  // Comp
  <el-dialog v-bind="$attrs">
    
  </el-dialog>
  <Comp v-model="visible"></Comp>
  // 给Comp传入v-model 自动实现了组件的v-model。因为 vue3的$attrs同时继承了attrs和emit
  
2.方法2
// vue3.4新出的defineModel()钩子,使用起来很简单
子组件中
  // Comp
  const visible = defineModel()
  <el-dialog v-model="visible">
    
  </el-dialog>
 <Comp v-model="visible"></Comp>
 
 // 这样写就实现了组件的v-model,代替了以前的computed劫持modelValue写法,其实他的原理实现基本逻辑也是定义了一个visible变量和watch变量时emit('update:modelValue')
 那么改变prop的默认值以及类型呢,也比较简单直接传入即可
 const visible = defineModel({
     type:Boolean,
     default:false,
     require:false
 })
 如何定义多个v-model,那么第一个参数就不是对象了,而是一个字符串
 const visible1 = defineModel('visible1',{type:Boolean})
 const visible2 = defineModel('visible2',{type:Boolean})
 <Comp v-model:visible1="visible1" v-model:visible2="visible2"/>

2.插槽处理

// Comp
  // 常规做法:循环$slots  声明插槽并将接收到的数据传递给 当前源组件的插槽(el-dialog)。 #[slot]是v-slot:[slot]的简写
  <el-dialog v-bind="$attrs">
    <template v-for="(_,slot) in $slots" #[slot]="slotProps">
      <slot :name="slot" v-bind="slotProps"></slot>
    </template>
    //如果组件有预设好的插槽 加上v-if即可
    <template #footer v-if="!$slots.footer">
        我是二次封装时预设好的插槽,此处内容从props中渲染。如果父组件没自定义footer时显示这个逻辑
    </template>
  </el-dialog>
// 新的尝试。上面的写法无非就是一段虚拟dom,那么就可以采用js的方式实现
<Comp></Comp>
<script lang="ts" setup>
// 用箭头函数包裹是因为,h函数在setup中是没有effecttive环境的。所以在函数调用时 是template中的render函数中,所以就是有响应式的
 const Comp = ()=>{
  h(ElDialog,useAttrs(),useSlots())
 }
// 或者直接使用动态组件 一行代码可以搞定。h函数从vue引入,ElDialog从element plus组件库引入,也可以是自己写的普通组件。
<component :is="h(ElDialog,$attrs,$slots)"></component>
</script>

3.暴露组件的方法

//常规做法1:创建一个instance。获取到组件的$.expoxed push进去。在defineExpose中导出。  
//常规做法2:合并组件的ref直接导出,带来的弊端是如果组件有v-if,那么compRef可能为null。
<script lang="ts" setup>
const obj = {}
const compRef = ref()
onMounted(() => {
  Object.assign(obj,compRef.value )
})
defineExpose({
  obj
})
</script>

----------------------
// 新的做法。返回一个代理函数,外部组件访问时会走到代理方式,
defineExpose({},{
    get(target,key){
        return compRef.value?.[key]
    },
    has(target,key){
        return key in compRef.value
    }
})

4.h函数的使用

1.定义

// h函数返回一个vnode.可以通过component is渲染到页面上。
// 接收3个参数  
// 第一个参数可以是 html元素 也可以是组件
// 第二个参数是对象 传递给div或组件的属性方法
// 第三个参数 则接收一个vnode的list 可以 渲染出来
const Comp = ()=>h('div', {
    style:{
      color: 'red'
    }
  },[h('span', {}, 'aaa'),h('span', {}, 'bbb')])
}


作为vnode使用:<component :is="Comp"></component>
作为组件使用: 
 <Comp :count="5">
  <div>我是插槽</div>
 </Comp>
如何接收参数呢
const Comp:FunctionalComponent<{ count: number}> = (props, ctx) => {
  return ()=>h('div', {
    style:{
      color: 'red'
    }
  },props.count)
}

2.作用域插槽和事件处理

 <Comp :count="5">
  <div>我是插槽</div>
  <template #footer="a">footer的插槽传参 {{ a }}</template>
 </Comp>
 
const Comp:FunctionalComponent<{ count: number}> = (props, {slots}) => {
  const a =ref('作用域插槽')
  return ()=>h('div', {
    onClick(){
      console.log('元素或者子组件传递过来的事件');
    },
    style:{
      color: 'red'
    }
  },[slots?.footer(a.value),'后面是默认插槽',  slots?.default?.() ])
}

3.h函数渲染组件时的处理

 <Comp >
  <div>我是默认插槽</div>
  <div>{{ props.msg }}</div>
  <slot name="footer" a="我是组件传递给插槽的值"></slot>
 </Comp>
 
 const Comp:FunctionalComponent<{ count: number}> = (props, {slots}) => {
  return ()=>h(HellowWorld, {
    msg:'我是传给helloword的值',
    onFoo(val){
      console.log('元素或者子组件传递过来的事件',val);
    }
  },{
    default:slots.default,
    footer:({a})=>h('div', {}, '自定义的footer插槽'+a)
  })
}
 

4.在vue3中的用法

// 写法1 
const Comp = defineComponent({
 name: 'HeaderMenu',
 props: {
   id: number,
 },
 render(props, ctx) {
   // ctx为上下文对象。包含了attrs emits和slots
   return ()=>h('div', {
       class:'my-class'
   }, ctx.$slots.default())
 }
})
// 写法2 
const Comp = defineComponent((props,ctx) => {
 return ()=>h('div', {}, ctx.$slots.default() + props.id,)
}, {
 id: number
})
// 写法3(推荐 省事儿)
const Comp = (props,ctx)=>{
    return ()=> h('div',{onFoo:(val)=>{console.log('子组件传过来的emit参数')}},'')
}
h函数的第二个参数可以是传递给属性的值或者传递给组件的参数。亦或者是监听组件的emit。
比如 Comp emit('onFoo')