详细分析Vue3中的props用法(父传子)

时间:2024-12-20 07:08:34

目录

  • 前言
  • 1. 基本知识
  • 2. Demo
    • 2.1 简单传值
    • 2.2 动态传值
    • 2.3 数组传值
    • 2.4 对象传值
  • 3. 实战
  • 4. 强化
    • 4.1 类型和验证
    • 4.2 响应性
    • 4.3 驼峰命名和kebab-case
    • 4.4 动态Prop

前言

对于子传父的组件推荐看此文:详细分析Vue3中的emit用法(子传父)

两种方式的差异:

特性 父传子 子传父
数据流向 从父组件到子组件 从子组件到父组件
机制 通过 props 传递数据 通过 emit 触发事件,父组件监听事件
适用场景 初始化数据、状态、配置 用户交互事件、表单提交等
实现方式 子组件定义 props,父组件绑定属性传递数据 子组件触发事件,父组件监听事件并处理数据
单向/双向 单向数据流 事件驱动(相对的双向通信,但仍然是单向的数据流)

1. 基本知识

在 Vue 3 中,父组件可以通过 props 向子组件传递数据

子组件通过 defineProps 或 props 来定义它接收的属性
这些属性在子组件中可以直接使用

// Options API
export default {
  props: {
    message: {
      type: String,
      required: true
    }
  }
}

// Composition API
<script setup>
const props = defineProps({
  message: {
    type: String,
    required: true
  }
});
</script>

父组件通过绑定属性的方式将数据传递给子组件

在这个示例中,父组件使用了 parentMessage 这个响应式变量,并将其传递给 ChildComponent 的 message 属性

<template>
  <div>
    <ChildComponent :message="parentMessage" />
  </div>
</template>

<script>
import ChildComponent from './';
import { ref } from 'vue';

export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  setup() {
    const parentMessage = ref('Hello from parent');

    return {
      parentMessage
    };
  }
}
</script>

通过 将父组件渲染出来,父组件会在模板中包含子组件,并通过 props 传递数据,形成完整的数据流

<template>
  <ParentComponent />
</template>

<script>
import ParentComponent from './components/';

export default {
  name: 'App',
  components: {
    ParentComponent
  }
}
</script>

2. Demo

针对多种情况,此处分类讨论:

2.1 简单传值

父组件:

<!--  -->
<template>
  <div>
    <ChildComponent message="Hello from parent" />
  </div>
</template>

<script>
import ChildComponent from './';

export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  }
}
</script>

子组件:

<!--  -->
<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script setup>
import { defineProps } from 'vue';

const props = defineProps({
  message: {
    type: String,
    required: true
  }
});
</script>

2.2 动态传值

父组件:

  • parentMessage 是一个 ref,表示父组件的消息数据
  • 使用 <ChildComponent :message="parentMessage" /> 语法将 parentMessage 的值作为 message 属性传递给子组件
<!--  -->
<template>
  <div>
    <ChildComponent :message="parentMessage" />
  </div>
</template>

<script setup>
import { ref } from 'vue';
import ChildComponent from './';

const parentMessage = ref('Hello from Parent!');
</script>

子组件:(和上述一致)

  • 通过 defineProps 定义 message 属性,指定其类型为 String,并且是必填项。
  • 使用 {{ message }} 语法在模板中显示传递过来的 message 值
<!--  -->
<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script setup>
import { defineProps } from 'vue';

const props = defineProps({
  message: {
    type: String,
    required: true
  }
});
</script>

2.3 数组传值

父组件:

<!--  -->
<template>
  <div>
    <ChildComponent :items="itemsArray" />
  </div>
</template>

<script>
import ChildComponent from './';
import { ref } from 'vue';

export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  setup() {
    const itemsArray = ref([1, 2, 3, 4, 5]);
    return {
      itemsArray
    };
  }
}
</script>

子组件:

<!--  -->
<template>
  <div>
    <ul>
      <li v-for="(item, index) in items" :key="index">{{ item }}</li>
    </ul>
  </div>
</template>

<script setup>
import { defineProps } from 'vue';

const props = defineProps({
  items: {
    type: Array,
    required: true
  }
});
</script>

2.4 对象传值

父组件:

<!--  -->
<template>
  <div>
    <ChildComponent :user="userObject" />
  </div>
</template>

<script>
import ChildComponent from './';
import { ref } from 'vue';

export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  setup() {
    const userObject = ref({
      name: 'John Doe',
      age: 30,
      email: '@'
    });
    return {
      userObject
    };
  }
}
</script>

子组件:

<!--  -->
<template>
  <div>
    <p>Name: {{  }}</p>
    <p>Age: {{  }}</p>
    <p>Email: {{  }}</p>
  </div>
</template>

<script setup>
import { defineProps } from 'vue';

const props = defineProps({
  user: {
    type: Object,
    required: true
  }
});
</script>

3. 实战

通过上述Demo结合实战,加深理解

父组件:传递formType和给EnterSiteForm组件

子组件 ():

  • 使用props接收formType和appointmentId
  • 根据formType的值决定输入框和按钮的禁用状态及显示状态
  • 当formType为’detail’时,禁用输入框,并隐藏“添加危险品”按钮和“删除”按钮

父组件如下:

<template>
  <Dialog>
    <!-- 子表的表单 -->
    <el-tabs v-model="subTabsName">
      <el-tab-pane label="危险品导入" name="enterSite">
        <EnterSiteForm ref="enterSiteFormRef" :appointment-id="" :form-type="formType" />
      </el-tab-pane>
    </el-tabs>
    <template #footer>
      <el-button @click="submitForm" type="primary" :disabled="formLoading">保存</el-button>
      <el-button @click="dialogVisible = false">取消</el-button>
    </template>
  </Dialog>
</template>

<script setup>
import { ref } from 'vue';

const formData = ref({
  id: 1 // 示例数据
});
const subTabsName = ref('enterSite');
const formType = ref('edit'); // 或 'detail'
const formLoading = ref(false);
const dialogVisible = ref(false);

const submitForm = () => {
  // 提交表单逻辑
};
</script>

子组件如下:

<template>
  <el-form>
    <el-table :data="tableData">
      <el-table-column label="危险品等级" min-width="150">
        <template #default="{ row, $index }">
          <el-form-item :prop="`${$index}.hazardousLevel`" :rules="" class="mb-0px!">
            <el-input v-model="" :disabled="formType === 'detail'" placeholder="请输入危险品等级" />
          </el-form-item>
        </template>
      </el-table-column>
      <el-table-column label="危规号" min-width="150">
        <template #default="{ row, $index }">
          <el-form-item :prop="`${$index}.hazardCode`" :rules="" class="mb-0px!">
            <el-input v-model="" :disabled="formType === 'detail'" placeholder="请输入危规号" />
          </el-form-item>
        </template>
      </el-table-column>
      <el-table-column align="center" fixed="right" label="操作" width="60">
        <template #default="{ $index }">
          <el-button v-if="formType !== 'detail'" @click="handleDelete($index)" link type="primary">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <el-row justify="center" class="mt-3">
      <el-button v-if="formType !== 'detail'" @click="handleAdd" round>+ 添加危险品</el-button>
    </el-row>
  </el-form>
</template>

<script setup lang="ts">
import { ref, defineProps } from 'vue';

const props = defineProps<{
  appointmentId: number | undefined,
  formType: string
}>();

const formRules = {
  hazardousLevel: [{ required: true, message: '请输入危险品等级', trigger: 'blur' }],
  hazardCode: [{ required: true, message: '请输入危规号', trigger: 'blur' }]
};

const tableData = ref([
  { hazardousLevel: '', hazardCode: '' }
]);

const handleAdd = () => {
  tableData.value.push({ hazardousLevel: '', hazardCode: '' });
};

const handleDelete = (index: number) => {
  tableData.value.splice(index, 1);
};
</script>

注意事项:

  • 要确保props属性能正确地传递和使用,尤其是在使用TypeScript时,需要明确定义所有必需的属性并正确地传递给子组件

  • 在代码中,需要确保props包含formType属性,并且需要对添加按钮和输入框的禁用状态以及显示状态进行正确的逻辑处理

4. 强化

根据上述Demo以及实战,强化补充知识

基本使用:

定义 Props:在子组件中,通过 props 选项或 defineProps(在组合式 API 中)定义需要从父组件接收的属性

<!-- 选项式 API -->
<script>
export default {
  props: ['title', 'likes'],
};
</script>

<!-- 组合式 API -->
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
  title: String,
  likes: Number,
});
</script>

传递 Props:在父组件中,通过在子组件标签上使用属性绑定的方式传递数据

<template>
  <ChildComponent :title="postTitle" :likes="postLikes" />
</template>

<script>
export default {
  data() {
    return {
      postTitle: 'Hello World',
      postLikes: 100,
    };
  },
};
</script>
  • 定义和传递:使用 props 选项或 defineProps 定义需要传递的属性,通过属性绑定在父组件中传递数据
  • 类型验证:使用类型、默认值、必填项以及自定义验证函数对 Prop 进行验证
  • 响应性:Prop 在子组件中是响应式的,但应避免直接修改 Prop 的值,可以通过本地状态或事件处理
  • 命名格式:Prop 名可以使用驼峰命名,但在模板中需要使用 kebab-case 形式
  • 动态绑定:使用 v-bind 或对象展开运算符动态传递 Pro

4.1 类型和验证

基本类型:可以定义 String、Number、Boolean、Array、Object、Function、Symbol 等基本类型

props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
}

默认值和必填:可以使用对象语法定义默认值、必填、类型验证等

props: {
  title: {
    type: String,
    required: true,
  },
  likes: {
    type: Number,
    default: 0,
  },
}

自定义验证器:可以提供一个函数进行自定义验证

props: {
  title: {
    type: String,
    validator: (value) => value.length > 5,
  },
}

4.2 响应性

响应式数据:父组件传递的响应式数据在子组件中也是响应式的,子组件修改 Prop 的值不会影响父组件的状态

<template>
  <p>{{ title }}</p>
</template>
<script>
export default {
  props: ['title'],
};
</script>

避免直接修改 Prop:子组件不应该直接修改 Prop 的值,可以使用本地副本或通过事件通知父组件修改

<script setup>
import { defineProps, ref, watch } from 'vue';

const props = defineProps({
  initialTitle: String,
});
const title = ref(props.initialTitle);

watch(() => props.initialTitle, (newVal) => {
  title.value = newVal;
});

const updateTitle = () => {
  title.value = 'New Title';
  // 使用事件通知父组件更新
  emit('update:title', title.value);
};
</script>

4.3 驼峰命名和kebab-case

这一点比较重要,一开始实战过程就是疏忽了这一点,导致一直传不过去

属性名格式:在模板中使用 Prop 时,驼峰命名的 Prop 需要使用 kebab-case 传递

// 子组件
props: {
  userName: String,
}

// 父组件
<ChildComponent :user-name="name" />

4.4 动态Prop

v-bind:可以使用 v-bind 动态绑定所有 Prop

<ChildComponent v-bind="postData" />

对象展开运算符:可以使用对象展开运算符传递多个 Prop

<ChildComponent v-bind="{ title, likes }" />