原则
- 保持单一职责
原则:每个组件应专注于完成一种特定功能,不要添加与组件主要功能无关的逻辑。
举例:封装一个按钮组件时,应该专注于样式和事件绑定,而不是处理复杂的表单逻辑。 - 尽量减少重复代码
原则:通过封装复用逻辑,减少多处使用同样代码的情况。
举例:可以对常用的输入框组件进行封装,将其通用功能提取为单一组件。 - 提供清晰的接口
原则:定义合理的 props 和 events,避免过多的参数,保持易用性和清晰度。
举例:组件需要的所有配置项应通过 props 提供,事件通过 $emit 通知父组件。 - 支持扩展性
原则:为封装组件提供插槽、动态样式或回调接口,让开发者可以扩展功能。
举例:提供 slot 和 props,使组件可以适应不同场景。 - 遵循团队规范
原则:封装组件时应符合团队的命名、代码风格和功能设计规范。
举例:Vue 组件的 props 使用驼峰命名,事件使用 update:modelValue 风格
方法
- 继承属性:使用
v-bind="$attrs"
来接收和传递父组件传递的属性。常用于向原组件传递未声明的属性。 - 继承事件:使用
emits
配置声明事件,或者通过$emit
进行事件的触发和传播。 - 继承方法:使用
ref
来暴露内部方法给父组件,通过defineExpose
明确暴露。 - 继承插槽:使用
slots
对象来接收和转发插槽内容。 - vue2与vue3的区别:在 vue3 中,取消了listeners这个组件实例的属性,将其事件的监听都整合到了attrs上,因此直接通过v-bind=$attrs属性就可以进行props属性和event事件的透传
在 vue2 中,需要用到 $slots
(插槽) 和 $scopedSlots
(作用域插槽)
在 vue3 中,取消了作用域插槽 $scopedSlots
,将所有插槽都统一在 $slots
当中
v-model
大家应该都知道v-model
只是一个语法糖,实际就是给组件定义了modelValue
属性和监听update:modelValue
事件,所以我们以前要实现数据双向绑定需要给子组件定义一个modelValue
属性,并且在子组件内要更新modelValue
值时需要emit
出去一个update:modelValue
事件,将新的值作为第二个字段传出去。
原因是因为从vue2
开始就已经是单向数据流,在子组件中是不能直接修改props
中的值。而是应该由子组件中抛出一个事件,由父组件去监听这个事件,然后去修改父组件中传递给props
的变量。如果这里我们给input
输入框直接加一个v-model="props.modelValue"
,那么其实是在子组件内直接修改props
中的modelValue
。
如果父组件和子组件中都使用了 v-model,并且绑定的是同一个变量,这个时候就会出问题了,因为子组件直接更改了父组件的数据,违背了单向数据流,这样会导致如果出现数据问题不好调试,无法定位出现问题的根源。
第一种方法:将 v-model 拆开,通过 emit 让父组件去修改数据
第二种方法:使用计算属性的 get set 方法
但是如果子组件中有多个表单项,不管是上面哪种方法,都要写很多重复的代码
xml
<!-- 父组件 -->
<template>
<my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
text: '',
password: '',
name: ''
})
</script>
<!-- 子组件 -->
<template>
<el-input v-model="name"></el-input>
<el-input v-model="text"></el-input>
<el-input v-model="password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
modelValue: {
type: Object,
default: () => {},
}
});
const emit = defineEmits(['update:modelValue']);
const name = computed(() => {
get() {
return props.modelValue.name
},
set(val) {
emit('update:modelValue', {
...props.modelValue,
name: val
})
}
})
const text = computed(() => {
get() {
return props.modelValue.text
},
set(val) {
emit('update:modelValue', {
...props.modelValue,
text: val
})
}
})
const password = computed(() => {
get() {
return props.modelValue.password
},
set(val) {
emit('update:modelValue', {
...props.modelValue,
password: val
})
}
})
</script>
上面使用计算属性监听单个属性,所以需要每个属性都写一遍,我们可以考虑在计算属性中监听整个对象:
xml
<!-- 父组件 -->
<template>
<my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
text: '',
password: '',
name: ''
})
</script>
<!-- 子组件 -->
<template>
<el-input v-model="modelList.name"></el-input>
<el-input v-model="modelList.text"></el-input>
<el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
modelValue: {
type: Object,
default: () => {},
}
});
const emit = defineEmits(['update:modelValue']);
const modelList = computed(() => {
get() {
return props.modelValue
},
set(val) {
emit('update:modelValue', val)
}
})
</script>
读取属性的时候能正常调用 get,但是设置属性的时候却无法触发 set,原因是 modelList.value = xxx
,才会触发 set,而 modelList.value.name = xxx
,无法触发。这个时候,Proxy
代理对象可以完美的解决这个问题:
xml
<!-- 父组件 -->
<template>
<my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
text: '',
password: '',
name: ''
})
</script>
<!-- 子组件 -->
<template>
<el-input v-model="modelList.name"></el-input>
<el-input v-model="modelList.text"></el-input>
<el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
modelValue: {
type: Object,
default: () => {},
}
});
const emit = defineEmits(['update:modelValue']);
const modelList = computed(() => {
get() {
return new Proxy(props.modelValue, {
get(target, key) {
return Reflect.get(target, key)
},
set(target, key, value) {
emit('update:modelValue',{
...target,
[key]: value
})
return true
}
})
},
set(val) {
emit('update:modelValue', val)
}
})
</script>
我们还可以考虑把这段代码进行封装,可以在多处引入进行使用: useVModel.ts
,其实 vueuse 里面有提供了这么一个方法,基本的逻辑是一样的。
useVModel
xml
<!-- 父组件 -->
<template>
<my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
text: '',
password: '',
name: ''
})
</script>
<!-- 子组件 -->
<template>
<el-input v-model="modelList.name"></el-input>
<el-input v-model="modelList.text"></el-input>
<el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useVModel } from './useVModel.ts'
const props = defineProps({
modelValue: {
type: Object,
default: () => {},
}
});
const emit = defineEmits(['update:modelValue']);
const modelList = useVModel(props, 'modelValue', emit)
</script>
使用defineModel实现数据双向绑定
defineModel是一个宏,所以不需要从vue中import导入,直接使用就可以了。这个宏可以用来声明一个双向绑定 prop,通过父组件的 v-model 来使用
xml
<template>
<CommonInput v-model="inputValue" />
</template>
<script setup lang="ts">
import { ref } from "vue";
const inputValue = ref();
</script>
ini
<template>
<input v-model="model" />
</template>
<script setup lang="ts">
const model = defineModel();
model.value = "xxx";
</script>
在上面的例子中我们直接将defineModel
的返回值使用v-model
绑定到input输入框上面,无需定义 modelValue
属性和监听 update:modelValue
事件,代码更加简洁。defineModel
的返回值是一个ref
,我们可以在子组件中修改model
变量的值,并且父组件中的inputValue
变量的值也会同步更新,这样就可以实现双向绑定。
原理:defineModel其实就是在子组件内定义了一个叫model的ref变量和modelValue的props,并且watch了props中的modelValue。当props中的modelValue的值改变后会同步更新model变量的值。并且当在子组件内改变model变量的值后会抛出update:modelValue事件,父组件收到这个事件后就会更新父组件中对应的变量值。
例子
xml
<template>
<my-input v-model="inputValue" ref="myInputRef" :name="'lyw'" :year="18" @input="onInput">
<!-- 动态插槽定义 -->
<template #prefix>
<span>Prefix Content</span>
</template>
<template #suffix="{ year }">
<span>END: {{ year }}</span>
</template>
<template #default="{ name }">
<p>Default Content: {{ name }}</p>
</template>
</my-input>
<div>当前输入的值是:{{ inputValue }}</div>
</template>
<script lang="ts" setup>
import MyInput from '@/components/MyInput.vue'
import { onMounted } from 'vue'
import { ref } from 'vue'
const inputValue = ref('')
const myInputRef = ref()
function onInput(value: string) {
console.log('Input value:', value)
}
onMounted(() => {
// 光标聚焦
// 避免ref 链式调用,比如 this.$refs.tableRef.$refs.table.clearSort()
// 前提:子组件把方法暴露
myInputRef.value.focus()
})
</script>
xml
<template>
<!-- 简化:直接使用 computed 绑定到 v-bind -->
<el-input v-model="localValue" v-bind="$attrs" ref="inputRef">
<!-- 动态插槽 -->
<template v-for="(_slot, slotName) in $slots" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps"></slot>
</template>
</el-input>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue'
import { computed, ref } from 'vue'
// 定义 props 和 emits
const props = defineProps({
modelValue: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue'])
// 使用 computed 来同步 props 和 emits
const localValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const inputRef = ref('')
let exposeObj: Record<string, any> = {}
const getMethod = () => {
const entries = Object.entries(inputRef.value)
for (const [method, fn] of entries) {
exposeObj[method] = fn
}
}
onMounted(getMethod)
defineExpose(exposeObj)
</script>