夜间模式CodeSnippetStyle:
日间模式CodeSnippetStyle:

0%


[Vue深入组件]:v-model语法糖与自定义v-model

1. v-model 语法糖

当你希望一个自定义组件的值能够实现双向绑定。 那么就需要:

  1. 将值传入组件;
  2. 将变化的值逆传回父组件。

实际上,就可以利用 props 实现的父传子 + 通过自定义事件this.$emit实现的子传父。实现双向的数据流传递。

下面是一个示例:

有这样一个父组件:

<template>
  <div>
    <Child :cusProp="message" @cusEvent="message = $event" />
    文字:{{message}}
  </div>

</template>
<script>
import Child from "./comps/child.vue"
export default {
  components: {
    Child
  },
  data() {
    return {
      message: 'init default'
    }
  }
}
</script>

和这样的一个子组件:

<template>
  <div>
    this is child comp
    <input type="text" :value="cusProp" @input="onInputChange">
  </div>
</template>
<script>
export default {
  props:["cusProp"],
  methods: {
    onInputChange(e) {
      this.$emit('cusEvent', e.target.value)
    }
  }
}
</script>

image-20210824220325859

我们自定义了一个组件,名为<Child /> , 我们通过 v-bind:cusProp<Child /> 传递了一个名为 "cusProp" 的prop , 即 <Child :cusProp="message" />

然后在<Child />组件内部,通过props接收到了这个值,并通过v-bind:cusProp 将值绑定给了<input /> 元素。

紧接着,我们给<input /> 元素设定了一个input 监听事件, 当输入时,触发该事件,然后将当前值通过this.$emit('cusProp',e.target.value) 触发了一个我们自定义命名为"cusProp"的自定义事件,以参数的形式,将变化后的值逆向传递(子传父)给了父组件。 在父组件中接收到变化后的值,然后通过$event 将值赋给了绑定的 message

从而实现了自定义的双向绑定。

实际上,上边这个过程,可以简化为一个vue为我们预定义实现的v-model, 但是不能直接替换,我们需要做一些简单的处理。这就涉及到了自定义v-model

2. 自定义v-model

2.1 v-model 语法糖, 以及最简单的自定义v-model

首先,我们仿照着vue文档的举例,尝试去理解需要自定义v-model的使用场景。

文档中有这样一段描述很重要

一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件

文档中的这段话极为概要,但是这句话蕴含了很重要的一些细节:

实际,当你使用v-model 的时候,默认是,是传递的名为valueprop ,且$emit触发的自定义事件的事件名是input

而回头看看我们刚才写的组件:

父组件:

 <Child :cusProp="message" @cusEvent="message = $event" />

子组件:

<input type="text" :value="cusProp" @input="onInputChange">
...
props:["cusProp"]
...
onInputChange(e) {
    this.$emit('cusEvent', e.target.value)
}

我们默认传递的prop值名为"cusProp", 即v-bind:cusProp , 且$emit触发的自定义事件名为cusEvent 。 并不满足能直接写作v-model 的形式其前提条件。 所以我们不能直接替换。

我们需要做一些简单的变化:

父组件:

<template>
  <div>
    <Child :value="message" @input="message = $event" /> <!--改动行-->
    文字:{{message}}
  </div>

</template>
<script>
import Child from "./comps/child.vue"
export default {
  components: {
    Child
  },
  data() {
    return {
      message: 'init default'
    }
  }
}
</script>

子组件:

<template>
  <div>
    this is child comp
    <input type="text" :value="value" @input="onInputChange"> <!--改动行-->
  </div>
</template>
<script>
export default {
  props:["value"],// --改动行--
  methods: {
    onInputChange(e) {
      this.$emit('input', e.target.value) //--改动行--
    }
  }
}
</script>

我们把prop 值的改为了value, 把$emit 触发的事件改为了input 现在,我们就能写作v-model 的 形式了,保持子组件不变,直接替换父组件中即可:

<Child v-model="message" />

自此,我们便能够理解,为什么说v-model 实际上就是props + $emit 自定义事件的语法糖 。

2.2 通用自定义v-model

上边的示例中,我们由于不满足先是利用了props 父传子,和自定义事件的子传父,手动实现了一个数据流的双向绑定。

紧接着,我们介绍了v-model 的实质,就是props + 自定义事件 的语法糖。 然后我们期望将我们自己的手动实现,简化成v-model语法糖的形式。

文档告诉我们,需要满足两个基本的默认条件:

  1. prop 名默认须为value
  2. $emit 触发的自定义事件名默认须为input

而我们的手动实现起初并不满足要求(prop ---- cusProp, $emit ---- cusEvent), 所以我们做了部分修改,以满足默认的条件。 从而实现了将手动实现,转换成了v-model语法糖的形式。

但是,这里有一个问题,就是v-model 默认的两个条件,会对我们有着很大的限制,这里封装的是一个<input/>输入框,以value prop值,以input 作为自定义事件名,本身是合乎习惯的,但是,日常开发中,我们不可能只封装一个输入框,可不能所有的自定义v-model 组件,都以value 传递,自定义事件一定名为input ,这显然是不合理的,也有违“自定义事件” 。我们开发工作中,可能更多的需要自定义指定prop名,和自定义事件。 为了更好的说明这个问题,解决通用性,下面我们通过一个示例来加深了解:

这里之所以要着重强调,是因为很容易出错,这个文档中说的input 事件到底指的是,自定义子组件中元素的监听事件名为input,还是说$emit触发的事件名为input。 以上就是为了强调,是后者,是$emit触发的事件名,默认情况下,必须为input。 尽管它是自定义事件名,这也是之所以容易出错的地方。

这里我们同样使用<input/ 这个元素,但是,不再用输入框了(type="text") ,我们将其指定为一个checkbox 看看会怎么样呢?

<!--Father Component-->
<template>
  <div>
    <Child :cusProp="status" @cusEvent="status = $event" />
    状态:{{status}}
  </div>

</template>
<script>
import Child from "../cusVModelcheckBox/comps/child.vue"
export default {
  components: {
    Child
  },
  data() {
    return {
      status: true
    }
  }
}
</script>
<!-- Child Component-->
<template>
  <div>
    this is child comp
    <input type="checkbox" :checked="cusProp" @change="onChange">
  </div>
</template>
<script>
export default {
  props:["cusProp"],
  methods: {
    onChange(e) {
      this.$emit('cusEvent', e.target.checked)
    }
  }
}

image-20210824220413039

一样的,如果此时,你想写作v-model语法糖的形式。就需要想刚才那样做一些改动:

父组件:

<template>
  <div>
    <Child v-model="status" /> <!--改动行-->
    状态:{{status}}
  </div>

</template>
<script>
import Child from "../cusVModelcheckBox/comps/child.vue"
export default {
  components: {
    Child
  },
  data() {
    return {
      status: true
    }
  }
}
</script>

子组件:

<template>
  <div>
    this is child comp
    <input type="checkbox" :checked="value" @change="onChange"> <!--改动行-->
  </div>
</template>
<script>
export default {
  props:["value"], //--改动行--
  methods: {
    onChange(e) {
      this.$emit('input', e.target.checked) //--改动行--
    }
  }
}

现在,由于这是一个checkbox ,我们可以放大刚才所描述的限制了。 你莫名奇妙的加上接受了一个名为value 的prop, 以及通过$emit 触发了一个莫名其妙的input自定义事件。 尽管它能够如期的正常工作。 当代码量多了之后, 你会发现这种组件异常难以维护。

那么到底该怎么解决这样一种场景呢?

其实非常简单 , 只需要指定一个model 对象属性即可:

我们只需要在刚才的基础上,在<Child/>组件中指定如下model配置加以稍微改动即可:

<template>
  <div>
    this is child comp
    <input type="checkbox" :checked="cusProp" @change="onChange">	<!--改动行-->
  </div>
</template>
<script>
export default {
  props:["cusProp"], 	//改动行 //当然一般直接写父组件v-model的变量名, 这里为了说明是任意名所以 写了个cusProp
  model:{				//改动行
    prop:'cusProp', 	//改动行
    event:'cusEvent'	//改动行
  },//改动行
  methods: {
    onChange(e) {
      this.$emit('cusEvent', e.target.checked)	//改动行
    }
  }
}

vue 为我们提供了一个名为model的实例配置项, 它可以指定一个任意的变量名,用于接受父组件中v-model 的传递值, 还可以指定一个任意的事件名, 用以"代理", $emit的触发事件.

这样,就解决了难以后期维护的问题,使得有双向绑定需求的组件封装更加的通用.

3. 总结

所以,总结一下。

什么情况下需要自定义v-model

  1. 当有自定义组件的双向数据流的需求的时候,都可以自定义v-model 来达成目的。

    1. 其中,什么时候需要配置 model 属性?

      当默认通过v-bind prop传递到自定义组件的变量名不是默认的value或者 触发自定义事件的事件名不为input 的时候。

    2. 什么时候不需要配置model 属性?

      当满足默认的v-model规则时,即 prop传递到自定义组件的变量名为value 触发自定义事件的事件名为input 的时候,不需要指定model属性配置。可直接使用v-model 这种情况比较少见,基本仅当自定义组件是为了扩展type="text"<input/> 元素时才符合条件。

特别注意的一点:

自定义事件内部,可以通过任意事件去触发$emit ,但是一般是通过DOM监听事件,例如@change@input@click,等等。 但是默认情况下,如果不配置model实例配置,加以指明,$emit 触发的事件名须是"input" 。 主要是不要混淆,这么默认情况下的约束规则,input 事件,指的是$emit触发的事件名,而不是自定义子组件内部触发$emit 的事件。

通过model 实例配置,实际上帮我们解决的主要问题是日后的维护问题,和代码易读性。 它相当于背后帮我们自动将默认propvalue ,默认自定义事件为input 做了一层别名化处理(alias),从而让我们能够去自定义任何名称。

4. 附加拓展,实践一个常见的v-model业务需求

【需求:】

假设现在有这样一个需求(基于antdv)

image-20210824222912833

有这样一个区域级联选择器,我希望,我能从父组件中给它一个初始值cascaderSelected:"浙江/杭州"。 在级联选择器值变换以后,这个cascaderSelected值响应式的变化。 要求利用v-model 实现,从而让代码简洁高效。

实现:

父组件:

<template>
  <div>
    <cus-area-cascader v-model="cascaderSelected"/>
    当前选中区域:{{cascaderSelected}}
  </div>

</template>
<script>
import CusAreaCascader from "../cusVModelPractice/comps/CusAreaCascader.vue"
export default {
  components: {
    CusAreaCascader
  },
  data() {
    return {
      cascaderSelected: ['zhejiang', 'hangzhou','xihu']
    }
  }
}
</script>

子组件:

<template>
  <a-cascader :options="options" :value="onPropHandle" placeholder="Please select" @change="onChange" />
</template>
<script>
export default {
  props:['onPropHandle'],
  model:{
    prop:'onPropHandle',
    event:'onChangeHandle'
  },
  data() {
    return {
      options: [
        {
          value: 'zhejiang',
          label: 'Zhejiang',
          children: [
            {
              value: 'hangzhou',
              label: 'Hangzhou',
              children: [
                {
                  value: 'xihu',
                  label: 'West Lake',
                },
              ],
            },
          ],
        },
        {
          value: 'jiangsu',
          label: 'Jiangsu',
          children: [
            {
              value: 'nanjing',
              label: 'Nanjing',
              children: [
                {
                  value: 'zhonghuamen',
                  label: 'Zhong Hua Men',
                },
              ],
            },
          ],
        },
      ],
    };
  },
  methods: {
    onChange(value) {
      this.$emit('onChangeHandle',value)
    },
  },
};
</script>

image-20210824224431745

目标达成。

posted @ 2021-08-24 22:46  暮冬有八  阅读(1145)  评论(1编辑  收藏  举报
BACK TO TOP

😀迷海无灯听船行。Github WeChat