自学内容网 自学内容网

Vue3学习记录(四)--- 组合式API之组件注册和组件数据交互

一、组件注册

​ 一个创建好的单文件组件(SFC)首先要进行注册,然后才能被别的组件使用。注册方法按照作用域区分为两种:全局注册和局部注册。

1、全局注册

​ 全局注册需要通过main.js中利用createApp()创建的Vue应用实例中提供的component(),对组件进行注册。注册完成后,该组件的适用范围是全局的,可以在当前项目的任一组件中使用全局组件。

component()方法需要两个参数,第一个参数为注册组件的组件名称,也就是如何在别的组件中使用该组件;第二个为引入的组件,表示组件的具体实现内容。

import { createApp } from 'vue'
// 导入要注册的组件
import Child from './components/Child.vue'
// 利用createApp()创建应用实例
const app = createApp(App)
// 使用component()注册全局组件
app.component('Child', Child)

component()方法可以被链式调用,注册多个全局组件,但注册的组件并不存在先后问题,全局组件之间可以彼此调用,在前面注册的组件中,也可以调用在后面注册的组件:

// 链式调用 注册多个全局组件
app.component('Child', Child).component('Child2', Child2)
局限:

​ 全局注册的组件,即使没有在页面中被使用,无任何引用关系,但还是会在生产打包时,被打包进去,影响打包速度和打包体积。而且全局注册的方式,会让组件间的关系没那么明确,不利于项目的长期维护。所以只建议将一些通用组件,如按钮、弹窗等组件进行全局注册。

2、局部注册

​ 局部注册需要在父组件中显式的导入要使用的组件,导入之后只能在当前父组件内使用,该父组件中的其他子组件不可以使用父组件中导入的组件。组件之间的依赖关系更明确,而且引入的组件没有在页面中被使用,在生产打包时会被自动移除 (也叫tree-shaking)。

​ 如果是使用<script setup>组合式API的组件,只要将子组件导入进来后,就可以直接在组件中使用:

<script setup>
  <!-- Child 表示的就是子组件在使用时的标签名 -->
import Child from '../components/Child.vue';
</script>

<template>
  <!-- 直接使用导入的子组件 -->
  <Child />
</template>

​ 如果是使用setup()钩子的组合式API,则需要在导入之后,使用components选项对组件进行注册之后,才能在组件使用,与选项式API相同。

3、组件命名格式

​ Vue官方推荐使用大驼峰的命名方式来命名组件,如PascalCase,因为这是一种合法的JavaScript标识符,在 JavaScript 中导入和注册组件都很容易。同时,<PascalCase />这种单标签闭合的形式,能够更好的表明这是一个Vue组件,而非原生HTML标签。

​ Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。这意味着一个以 MyComponent 为名注册的组件,在模板中可以通过 <MyComponent><my-component> 两种方式使用。

二、父子组件之间传值

1、父向子传值(Props)
基础相关:

​ 父组件想要向子组件传递数据,需要借助Props来实现。在使用<script setup>组合式API的子组件中,需要使用defineProps()宏方法(宏方法无需导入,可直接使用),显式的声明当前组件所接收的数据名称,如果父组件传递的数据没有在子组件中使用该方法声明,则传递的数据不会被子组件接收到。

​ 父组件以attribute的形式向子组件传递数据,可以是静态数据,也可以是动态数据,如响应式变量、js表达式等等:

<!-- 静态数据 -->
<Child titleText="啊啊啊啊啊啊"></Child>
<!-- 动态数据-响应式变量 -->
<Child :titleText="text"></Child>
<!-- 动态数据-js表达式 -->
<Child :titleText="text + 'add' + text2"></Child>

​ 子组件依靠defineProps()方法接收传递的数据,该方法会返回一个props对象,在JS中可以通过该对象来访问接收的数据:props.数据名,但是在组件模板中,可以无需借助props,直接以数据名的形式访问。

defineProps()方法的参数可以是一个数组['数据名1','数据名2',...],数组元素为字符串,表示接受的数据名称,多个名称则表示可以接受多个数据:

<template>
  <div> 
    <!-- 在模板中使用props中的数据 -->
    <p>{{ titleText }}</p>
  </div>
</template>

<script setup>
// 使用宏方法定义要接受的数据
const props = defineProps(['titleText'])
// 在JS中访问props的中的数据 
console.log(props.titleText)
</script>
父组件同时传递多条数据:

​ 在父组件中利用v-bind将一个对象绑定到子组件上,可以将该对象中的所有属性都作为props传递给子组件,这个对象可以是普通的JS对象,也可以是一个响应式对象 :

// 要传递的普通对象
const obj = {
  name: 'zujianhua',
  age: 18,
  sex: '男'
}
// 要传递的响应式对象
const obj2 = ref(obj)

绑定到子组件上:

<Child :titleText="11111" v-bind="obj"></Child>
<Child2 :titleText="11111" v-bind="obj2"></Child2>
<!-- -------------- -->
<!-- 上面的代码等价于下面的 -->
<!-- -------------- -->
<Child :titleText="11111" :name="obj.name" :age="obj.age" :sex="obj.sex"></Child>
<Child2 :titleText="11111" :name="obj2.name" :age="obj2.age" :sex="obj2.sex"></Child2
子组件指定数据类型:

defineProps()方法的参数还可以是对象形式,其中对象属性key是要接收的数据名,属性值value则是要接收数据的类型的构造函数名,如:字符串类型对应的是String、数值类型对应Number、布尔类型对应Boolean。属性值value也可以是对象形式,属性值对象中type字段的值表示数据的类型

​ 支持的数据类型:StringNumberBooleanArrayObjectDateSymbolFunctionError等。

​ 如果父组件传输的数据类型与子组件指定的数据类型不符,则控制台中会抛出警告:

<script setup>
// 使用宏方法定义要接受的数据
const props = defineProps({
  titleText: String // 定义类型为字符串
});
// 等同于
const props = defineProps({
  titleText: {
    type: String// 定义类型为字符串
  } 
});
// 在JS中访问props的中的数据 
console.log(props.titleText);
</script>

​ 属性值value还可以是一个数组,其中包含多种类型,表示该数据可以是多类型的:

const props = defineProps({
  titleText: [String, Number] // 类型可以是字符串 也可以是数值
})
子组件指定数据必传:

​ 在子组件中可以设置props中的某些数据为必填项,只需要在数据的属性值对象的中添加required: true;属性即可:

const props = defineProps({
  titleText: {
    type: String, // 定义类型为字符串
    required: true // 定义数据为必传项
  } 
});
子组件指定数据默认值:

​ 如果父组件未传输某个数据,则除布尔类型的值默认为false外,其余类型的数据的值都默认为undefined,但在子组件中可以设置props中的某些数据的默认值,只需要在数据的属性值对象的中添加default属性。当父组件没有传递该数据时,则采用默认值。简单类型的数据,可以直接指定数据的默认值:

const props = defineProps({
  titleText: {
    type: String, // 定义类型为字符串
    default: '默认String' // 定义数据的默认值
  } 
});

​ 复杂数据类型的数据,则需要通过一个工厂函数返回数据的默认值,这可以避免由于数据引用相同,导致组件实例之间互相干扰的问题。通过工厂函数返回的默认值,可以保证每个组件实例获取的都是一个独立的默认值:

const props = defineProps({
  myObject: {
    type: Object,
    default: () => ({ key: 'value' }) // 使用工厂函数返回对象
  },
  myArray: {
    type: Array,
    default: () => [1, 2, 3] // 使用工厂函数返回数组
  }
});
子组件中进行数据校验:

​ 在子组件中可以通过validator属性自定义数据的校验函数,该函数接收两个参数,第一个参数为父组件传给该字段的值,第二个参数为props本身,是在 v3.4 版本中新增的参数,可以访问props中的数据。如果父组件传递的数据没有通过校验,则校验函数返回false;如果通过校验,则校验函数返回true

const props = defineProps({
  titleText: {
    type: String, // 指定数据类型
    // 执行数据校验函数 value-表示数据本身的值 props-可以访问其他数据
    validator: (value,props) => {
      const maxLength = 5;
      const typeArr = ['1111', '2222', '3333']
      return value.length < maxLength && typeArr.includes(value)
    }
  },
})

​ 虽然可以给传递的数据进行校验,但这种校验并非强制性校验,即使校验结果为false,也只会在开发模式下在控制台进行警告,但代码依旧能继续执行,不会中断。

单向数据流:

​ 父子组件之间所有的props数据传递,都必须遵循单向绑定的原则,父组件中的数据发生更新,会自动传递给子组件,但不能在子组件中直接修改props接收的数据。

​ 如果props接收的数据是对象或者数组类型时,由于这两种复杂数据类型是按引用进行传递的,因此在子组件确实可以做到更改其内部的属性值。但并不推荐进行这种操作,因为很可能会引起较大的性能损耗,而且不利于代码的清晰性和可读性。

​ 如果是想要将props中的数据作为一个初始值,并在后续操作中进行修改,可以重新定义一个响应式变量,并以props中的数据值作为初始值,然后对该变量进行后续操作即可:

const props = defineProps(['num'])

// 计数器只是将 props.num 作为初始值
// 后续更新直接使用counter即可
const counter = ref(props.num)

​ 如果是想要根据props中的数据进行运算,并根据传递过来的数据进行实时更新,则可以借助计算属性实现:

const props = defineProps(['size'])

// 该 prop 变更时计算属性也会自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())
2、子向父传值(emit)

​ 子组件向父组件传递数据需要借助自定义事件,子组件触发自定义事件,父组件监听自定义事件,然后通过自定义事件绑定的方法和参数,获取到子组件传递的数据。根据自定义触发位置的不同,分为以下两种情况:在组件模板中触发和在<script setup>中触发。

在组件模板中触发自定义事件:

​ 在子组件的组件模板中,我们可以在模板元素的各种事件中,通过模板表达式结合$emit()方法来直接触发自定义事件,方法的第一个参数为自定义事件的名称:

<!-- 在组件模板中触发自定义事件 -->
<button @click="$emit('countAdd')">模板触发自定义事件</button>

​ 如果想要在触发自定义事件时,传递数据,则可以通过$emit()方法的后续参数来传递数据,第一个参数用于指定事件名称,后续所有参数都会被作为数据被传递到父组件中:

<!-- 数据 2 会被传递到父组件中 -->
<button @click="$emit('countAdd', 2)">模板触发自定义事件2</button>
<!-- 数据 1、2、3 会被传递到父组件中 -->
<button @click="$emit('countAdd', 1, 2, 3)">模板触发自定义事件3</button>
<script setup>中触发:

​ 在子组件的<script setup>中,需要通过 defineEmits()宏方法(宏方法无需导入,可直接使用),可以数组的形式['事件名1','事件名2',...],显式的声明要触发的自定义事件名称。 如果子组件触发了一个未声明的自定义事件,父组件依旧能够监听到该事件,但控制台会报警告,也不利于代码的逻辑可读性。所以还是推荐完整地声明所有要触发的事件。

defineEmits()宏方法必须放置在<script setup>的顶级作用域下,不能在子函数中使用,且该函数会返回一个emit()方法,后续需要通过返回的这个方法,来触发已经声明的自定义函数,方法的第一个参数为自定义事件的名称:

<!-- 绑定事件处理函数,在处理函数中触发自定义事件 -->
<button @click="buttonClick">JS触发自定义事件3</button>

<script setup>
// 调用 defineEmits 声明一个自定义事件,并获取一个触发器函数
const emit = defineEmits(['countAdd'])

// 绑定的事件处理函数
function buttonClick() {
  // 通过触发器函数触发自定义事件
  emit('countAdd')
}
</script>

​ 如果想要在触发自定义事件时,传递数据,则可以通过返回的emit()方法的后续参数来传递数据,第一个参数用于指定事件名称,后续所有参数都会被作为数据被传递到父组件中:

// 绑定的事件处理函数
function buttonClick() {
  // 通过触发器函数触发自定义事件 并传递数据 2
  emit('countAdd', 2);
}

// 绑定的事件处理函数
function buttonClick() {
  // 通过触发器函数触发自定义事件 并传递数据 1、2、3
  emit('countAdd', 1, 2, 3);
}
父组件监听自定义事件:

​ 在父组件中,只需要子组件的模板标签上,通过v-on@来监听子组件中的自定义事件名,并绑定相应事件处理函数即可:

<!-- 父组件中监听子组件的自定义事件 可以使用camelCase 形式 -->
<Child @count-add="countAdd"></Child>
<!-- 也可以使用kebab-case 形式 -->
<Child @countAdd="countAdd"></Child>

<script setup>
import { ref } from 'vue';
// 声明响应式变量
const count = ref(0)
// 事件处理函数中进行逻辑处理
const countAdd = () => {
  count.value++;
}
</script>

​ 如果子组件的自定义事件传递了数据,则可以在父组件中的事件处理函数中,通过函数参数的形式,一一对应的获取子组件传递的数据:

<!-- 传递单个数据 -->
<Child @count-add2="countAdd2"></Child>
<!-- 传递多个数据 -->
<Child @count-add3="countAdd3"></Child>

<script setup>
import { ref } from 'vue';
// 声明响应式变量
const count = ref(0)

// 子组件的自定义事件的处理函数
// n是子组件传递过来的数据 对应emit()的第二个参数
const countAdd2 = (n) => {
  count.value+=n;
}
// 子组件的自定义事件的处理函数
// n是子组件传递过来的数据 对应emit()的第二个参数
// s是子组件传递过来的数据 对应emit()的第三个参数
const countAdd3 = (n,s) => {
  console.log(n,s);
  count.value+=n;
}
</script>

​ 如果子组件中自定义事件的名称与原生事件的名称相同,则父组件中只能监听到子组件触发的自定义事件,而不会再触发相同名称的原生事件。也就是说在名称冲突的情况下,自定义事件的优先级高于原生事件的优先级。

事件校验:

​ 与props类似,子组件中通过 defineEmits()方法,也可以对象的形式来描述声明的自定义事件,对象属性名为自定义事件名,对象的属性值为事件的校验函数。函数的参数为事件传递的数据,函数的返回值决定了事件是否合法。如果自定义事件无需校验,则直接设置属性值为null即可。

// 事件校验
const emit = defineEmits({
  // 不进行校验
  countAdd: null,
  // 校验传递的参数是否大于0
  countAdd2: (n) => {
    return n > 0
  },
})

​ 虽然可以给自定义事件进行校验,但这种校验并非强制性校验,即使校验结果为false,代码依旧能继续执行,不会中断。

3、父子组件双向绑定
基本使用:

​ 父子组件之间除了通过props+emit的方式进行数据交互之外,还可以通过v-model实现数据在父子组件之间的双向绑定,在子组件中修改传递的数据,父组件中数据的值也会跟着变。

​ 在Vue的 v3.4 及之后版本推荐使用defineModel()宏方法(宏方法无需导入,可直接使用)结合v-model来实现双向绑定功能。父组件中使用v-model向子组件绑定一个数据:

<!-- 父组件展示绑定数据 -->
<p>{{ count }}</p>
<!-- 父组件使用v-model向子组件绑定数据 -->
<Child v-model="count"></Child>

​ 子组件中调用defineModel()宏方法,接收父组件绑定的数据。defineModel()返回值是一个ref响应式变量,可以被访问和修改,而且该变量的值.value与父组件v-model绑定的变量值同步更新:

<!-- 修改父子组件双向绑定数据值 -->
<button @click="changeModel">修改父子组件双向绑定数据值</button>
<!-- 展示双向绑定数据值 -->
<h5>{{ modelVar }}</h5>

<script setup>
  // 使用defineModel()获取父组件传递的双向绑定数据
const modelVar = defineModel();
// 修改双向绑定数据的值 父组件中的数据同步变化
const changeModel = () => {
    modelVar.value++
}
</script>

defineModel()宏方法是一个便利宏,其相当于下面这种实现方式的简写:

  • 一个名为modelValueprop,本地有一个ref与其值同步。
  • 一个名为update:modelValue的自定义事件,在本地的ref值发生变化时触发。
<!-- 修改父子组件双向绑定数据值2 -->
<button @click="changeModel">修改父子组件双向绑定数据值</button>
<!-- 展示父组件传递过来的数据 -->
<h5>{{ props.modelValue }}</h5>

<script setup>
import {  ref } from 'vue';
  
// 接收一个名为modelValue的prop
const props = defineProps(['modelValue'])
// 创建一个名为update:modelValue的自定义事件
const emit = defineEmits(['update:modelValue'])
// 创建一个与props.modelValue的值同步的ref
const modelVar = ref(props.modelValue);

// 修改双向绑定数据的值
const changeModel = () => {
  // 修改本地变量的值
  modelVar.value++
  // 使用自定义事件修改父组件中数据的值
  emit('update:modelValue',modelVar.value)
}
</script>

​ 在Vuev3.4 之前版本中我们可以上面这种方式来实现父子组件中数据的双向绑定。

设置v-model的参数:

​ 父组件中的v-model后面可以跟随一个参数,表示子组件defineModel('数据名称')方法中对应的数据名称:

<!-- 父组件使用v-model向子组件绑定数据 并设置数据名称 -->
<Child v-model:count="count"></Child>

​ 子组件中需要使用defineModel('数据名称')的方式来接收对应的数据:

const modelVar = defineModel('count')
绑定多个v-model

​ 父组件想要绑定多个v-model,则需要给每一个v-model后面都跟随一个参数,给每一个v-model设置一个单独的名称:

<!-- 父组件使用v-model向子组件绑定多个数据 并一一设置数据名称 -->
<Child v-model:count="count" v-model:title="text"></Child>

在子组件中使用defineModel('数据名称')依次接受传入的数据:

// defineModel() 方法中的数据名称要与父组件中v-model后面跟随的数据名称一一对应
const modelVar = defineModel('count')
const title = defineModel('title')
自定义v-model修饰符:

​ 在父子组件之间通过v-model实现双向数据绑定时,我们可以结合defineModel()自定义修饰符,对双向绑定的数据进行修饰。

​ 首先需要在父组件的v-model后面增加自定义修饰符的名称:

<!-- 自定义test修饰符 -->
<Child v-model.test="text"></Child>

​ 然后在子组件中通过解构,获取 defineModel() 返回值中的数据值和modifiers修饰符对象:

// 获取数据值和modifiers对象
const [modelVar,modifiers] = defineModel()
// 输出数据值
console.log(modelVar.value); // "title"
// 输出modifiers对象 使用了多少修饰符 就会有多少个字段
console.log(modifiers); // { test: true }

​ 最后是给自定义修饰符制定修饰逻辑,通过给 defineModel() 传入set()方法选项,并在方法中对数据进行修饰。当双向绑定的数据在子组件中被修改时(不包括初始获取值),就会触发set()方法,执行相应的修饰逻辑:

// 获取数据值和modifiers对象
const [modelVar,modifiers] = defineModel({
  // 传入set()函数 value是最新的值
  set(value) {
    // 判断是否使用了目标修饰符
    if(modifiers.test) {
      console.log('set');
      // 将数据进行处理后返回
      return value.charAt(0).toUpperCase() + value.slice(1);
    }
    // 未使用则直接返回
    return value;
  }
})

原文地址:https://blog.csdn.net/weixin_45092437/article/details/136457079

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!