引言

在现代前端开发中,组件化开发已经成为主流。Vue.js、React等框架都支持将UI拆分为独立可复用的组件。然而,组件之间并非孤立存在,它们需要通过数据传递来协同工作。父子组件通信是组件间通信中最基础、最常见的一种形式。本文将从基础概念讲起,逐步深入到进阶技巧,并通过丰富的实战案例,帮助你全面掌握父子组件通信的精髓。

一、基础篇:单向数据流与Props

1.1 什么是单向数据流?

在Vue和React中,数据流动遵循“单向数据流”原则。这意味着数据总是从父组件流向子组件,子组件不能直接修改父组件传递过来的props。这种设计使得应用的数据流向更加清晰,便于追踪和调试。

1.2 父组件向子组件传递数据(Props)

这是最基础的通信方式。父组件通过props将数据传递给子组件。

Vue示例:

<!-- 父组件 Parent.vue -->
<template>
  <div>
    <h1>父组件</h1>
    <ChildComponent :message="parentMessage" :user="userData" />
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  components: { ChildComponent },
  data() {
    return {
      parentMessage: 'Hello from Parent!',
      userData: {
        name: '张三',
        age: 25
      }
    }
  }
}
</script>
<!-- 子组件 ChildComponent.vue -->
<template>
  <div>
    <h2>子组件</h2>
    <p>接收到的消息:{{ message }}</p>
    <p>用户信息:{{ user.name }},{{ user.age }}岁</p>
  </div>
</template>

<script>
export default {
  props: {
    message: {
      type: String,
      required: true
    },
    user: {
      type: Object,
      required: true
    }
  }
}
</script>

React示例:

// 父组件 Parent.js
import React from 'react';
import ChildComponent from './ChildComponent';

function Parent() {
  const parentMessage = 'Hello from Parent!';
  const userData = {
    name: '张三',
    age: 25
  };

  return (
    <div>
      <h1>父组件</h1>
      <ChildComponent message={parentMessage} user={userData} />
    </div>
  );
}

export default Parent;
// 子组件 ChildComponent.js
import React from 'react';

function ChildComponent({ message, user }) {
  return (
    <div>
      <h2>子组件</h2>
      <p>接收到的消息:{message}</p>
      <p>用户信息:{user.name},{user.age}岁</p>
    </div>
  );
}

export default ChildComponent;

1.3 Props验证

为了确保数据的正确性,我们应该对props进行验证。

Vue Props验证示例:

props: {
  // 基础类型检查
  message: String,
  
  // 多个可能的类型
  age: [String, Number],
  
  // 必填项
  name: {
    type: String,
    required: true
  },
  
  // 带有默认值
  count: {
    type: Number,
    default: 0
  },
  
  // 对象或数组的默认值必须由一个工厂函数返回
  user: {
    type: Object,
    default: () => ({
      name: '默认用户',
      age: 18
    })
  },
  
  // 自定义验证函数
  score: {
    type: Number,
    validator: value => {
      return value >= 0 && value <= 100;
    }
  }
}

React PropTypes验证示例:

import PropTypes from 'prop-types';

function ChildComponent({ message, user }) {
  // 组件实现...
}

ChildComponent.propTypes = {
  message: PropTypes.string.isRequired,
  user: PropTypes.shape({
    name: PropTypes.string.isRequired,
    age: PropTypes.number
  }).isRequired,
  count: PropTypes.number,
  score: (props, propName, componentName) => {
    if (props[propName] < 0 || props[propName] > 100) {
      return new Error(
        `${componentName}: ${propName} 必须在0到100之间`
      );
    }
  }
};

ChildComponent.defaultProps = {
  count: 0,
  user: {
    name: '默认用户',
    age: 18
  }
};

1.4 实战案例:商品列表组件

让我们通过一个商品列表的案例来巩固基础概念。

Vue实现:

<!-- 父组件 ProductList.vue -->
<template>
  <div class="product-list">
    <h2>商品列表</h2>
    <ProductItem 
      v-for="product in products" 
      :key="product.id"
      :product="product"
      :show-stock="true"
    />
  </div>
</template>

<script>
import ProductItem from './ProductItem.vue'

export default {
  components: { ProductItem },
  data() {
    return {
      products: [
        { id: 1, name: 'iPhone 15', price: 5999, stock: 10 },
        { id: 2, name: 'MacBook Pro', price: 12999, stock: 5 },
        { id: 3, name: 'AirPods Pro', price: 1899, stock: 20 }
      ]
    }
  }
}
</script>
<!-- 子组件 ProductItem.vue -->
<template>
  <div class="product-item">
    <h3>{{ product.name }}</h3>
    <p>价格:¥{{ product.price }}</p>
    <p v-if="showStock">库存:{{ product.stock }}件</p>
    <button @click="addToCart">加入购物车</button>
  </div>
</template>

<script>
export default {
  props: {
    product: {
      type: Object,
      required: true
    },
    showStock: {
      type: Boolean,
      default: false
    }
  },
  methods: {
    addToCart() {
      console.log(`添加商品:${this.product.name}`);
      // 这里只是演示,实际应该通过事件向父组件通信
    }
  }
}
</script>

React实现:

// 父组件 ProductList.js
import React from 'react';
import ProductItem from './ProductItem';

function ProductList() {
  const products = [
    { id: 1, name: 'iPhone 15', price: 5999, stock: 10 },
    { id: 2, name: 'MacBook Pro', price: 12999, stock: 5 },
    { id: 3, name: 'AirPods Pro', price: 1899, stock: 20 }
  ];

  return (
    <div className="product-list">
      <h2>商品列表</h2>
      {products.map(product => (
        <ProductItem 
          key={product.id}
          product={product}
          showStock={true}
        />
      ))}
    </div>
  );
}

export default ProductList;
// 子组件 ProductItem.js
import React from 'react';

function ProductItem({ product, showStock }) {
  const addToCart = () => {
    console.log(`添加商品:${product.name}`);
    // 这里只是演示,实际应该通过事件向父组件通信
  };

  return (
    <div className="product-item">
      <h3>{product.name}</h3>
      <p>价格:¥{product.price}</p>
      {showStock && <p>库存:{product.stock}件</p>}
      <button onClick={addToCart}>加入购物车</button>
    </div>
  );
}

export default ProductItem;

二、进阶篇:子组件向父组件通信

2.1 事件机制

子组件不能直接修改父组件的数据,但可以通过触发事件的方式通知父组件,由父组件来决定如何更新数据。

Vue示例:

<!-- 父组件 Parent.vue -->
<template>
  <div>
    <h1>父组件</h1>
    <p>子组件计数:{{ childCount }}</p>
    <ChildComponent 
      :initial-count="5"
      @count-updated="handleCountUpdate"
      @custom-event="handleCustomEvent"
    />
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  components: { ChildComponent },
  data() {
    return {
      childCount: 0
    }
  },
  methods: {
    handleCountUpdate(newCount) {
      this.childCount = newCount;
      console.log(`父组件收到更新:${newCount}`);
    },
    handleCustomEvent(payload) {
      console.log('自定义事件携带的数据:', payload);
    }
  }
}
</script>
<!-- 子组件 ChildComponent.vue -->
<template>
  <div>
    <h2>子组件</h2>
    <p>当前计数:{{ count }}</p>
    <button @click="increment">增加</button>
    <button @click="decrement">减少</button>
    <button @click="sendCustomEvent">发送自定义事件</button>
  </div>
</template>

<script>
export default {
  props: {
    initialCount: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      count: this.initialCount
    }
  },
  methods: {
    increment() {
      this.count++;
      this.$emit('count-updated', this.count);
    },
    decrement() {
      this.count--;
      this.$emit('count-updated', this.count);
    },
    sendCustomEvent() {
      // 发送带有复杂数据的自定义事件
      this.$emit('custom-event', {
        timestamp: Date.now(),
        action: 'custom-button-clicked',
        data: { value: Math.random() }
      });
    }
  }
}
</script>

React示例:

// 父组件 Parent.js
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';

function Parent() {
  const [childCount, setChildCount] = useState(0);

  const handleCountUpdate = (newCount) => {
    setChildCount(newCount);
    console.log(`父组件收到更新:${newCount}`);
  };

  const handleCustomEvent = (payload) => {
    console.log('自定义事件携带的数据:', payload);
  };

  return (
    <div>
      <h1>父组件</h1>
      <p>子组件计数:{childCount}</p>
      <ChildComponent 
        initialCount={5}
        onCountUpdated={handleCountUpdate}
        onCustomEvent={handleCustomEvent}
      />
    </div>
  );
}

export default Parent;
// 子组件 ChildComponent.js
import React, { useState } from 'react';

function ChildComponent({ initialCount = 0, onCountUpdated, onCustomEvent }) {
  const [count, setCount] = useState(initialCount);

  const increment = () => {
    const newCount = count + 1;
    setCount(newCount);
    if (onCountUpdated) {
      onCountUpdated(newCount);
    }
  };

  const decrement = () => {
    const newCount = count - 1;
    setCount(newCount);
    if (onCountUpdated) {
      onCountUpdated(newCount);
    }
  };

  const sendCustomEvent = () => {
    if (onCustomEvent) {
      onCustomEvent({
        timestamp: Date.now(),
        action: 'custom-button-clicked',
        data: { value: Math.random() }
      });
    }
  };

  return (
    <div>
      <h2>子组件</h2>
      <p>当前计数:{count}</p>
      <button onClick={increment}>增加</button>
      <button onClick={decrement}>减少</button>
      <button onClick={sendCustomEvent}>发送自定义事件</button>
    </div>
  );
}

export default ChildComponent;

2.2 实战案例:表单验证组件

让我们通过一个表单验证组件来展示子组件向父组件通信的实际应用。

Vue实现:

<!-- 父组件 RegistrationForm.vue -->
<template>
  <div class="registration-form">
    <h2>用户注册</h2>
    <form @submit.prevent="handleSubmit">
      <ValidatedInput 
        label="用户名"
        type="text"
        :value="formData.username"
        @input="updateField('username', $event)"
        @validate="handleValidation('username', $event)"
        :rules="usernameRules"
      />
      
      <ValidatedInput 
        label="邮箱"
        type="email"
        :value="formData.email"
        @input="updateField('email', $event)"
        @validate="handleValidation('email', $event)"
        :rules="emailRules"
      />
      
      <ValidatedInput 
        label="密码"
        type="password"
        :value="formData.password"
        @input="updateField('password', $event)"
        @validate="handleValidation('password', $event)"
        :rules="passwordRules"
      />
      
      <button type="submit" :disabled="!isFormValid">注册</button>
    </form>
    
    <div v-if="submitResult" class="result">
      <h3>提交结果</h3>
      <pre>{{ JSON.stringify(submitResult, null, 2) }}</pre>
    </div>
  </div>
</template>

<script>
import ValidatedInput from './ValidatedInput.vue'

export default {
  components: { ValidatedInput },
  data() {
    return {
      formData: {
        username: '',
        email: '',
        password: ''
      },
      validationErrors: {
        username: '',
        email: '',
        password: ''
      },
      usernameRules: [
        { validator: value => value.length >= 3, message: '用户名至少3个字符' },
        { validator: value => /^[a-zA-Z0-9_]+$/.test(value), message: '用户名只能包含字母、数字和下划线' }
      ],
      emailRules: [
        { validator: value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), message: '请输入有效的邮箱地址' }
      ],
      passwordRules: [
        { validator: value => value.length >= 8, message: '密码至少8个字符' },
        { validator: value => /[A-Z]/.test(value), message: '密码必须包含大写字母' },
        { validator: value => /[0-9]/.test(value), message: '密码必须包含数字' }
      ],
      submitResult: null
    }
  },
  computed: {
    isFormValid() {
      return Object.values(this.validationErrors).every(error => !error);
    }
  },
  methods: {
    updateField(field, value) {
      this.formData[field] = value;
    },
    handleValidation(field, error) {
      this.validationErrors[field] = error;
    },
    handleSubmit() {
      if (this.isFormValid) {
        this.submitResult = {
          success: true,
          data: { ...this.formData },
          timestamp: new Date().toISOString()
        };
        console.log('表单提交成功:', this.formData);
      } else {
        this.submitResult = {
          success: false,
          errors: { ...this.validationErrors }
        };
        console.log('表单验证失败:', this.validationErrors);
      }
    }
  }
}
</script>
<!-- 子组件 ValidatedInput.vue -->
<template>
  <div class="validated-input">
    <label>{{ label }}</label>
    <input 
      :type="type"
      :value="value"
      @input="handleInput"
      @blur="validate"
      :class="{ 'error': hasError }"
    />
    <div v-if="hasError" class="error-message">{{ errorMessage }}</div>
  </div>
</template>

<script>
export default {
  props: {
    label: {
      type: String,
      required: true
    },
    type: {
      type: String,
      default: 'text'
    },
    value: {
      type: String,
      required: true
    },
    rules: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      errorMessage: '',
      hasError: false
    }
  },
  methods: {
    handleInput(event) {
      const value = event.target.value;
      this.$emit('input', value);
      // 每次输入都清除错误状态
      this.errorMessage = '';
      this.hasError = false;
      this.$emit('validate', '');
    },
    validate() {
      if (!this.value && this.rules.length > 0) {
        // 如果是必填项但为空,显示第一条规则的错误信息
        this.errorMessage = this.rules[0].message;
        this.hasError = true;
        this.$emit('validate', this.errorMessage);
        return;
      }
      
      for (const rule of this.rules) {
        if (!rule.validator(this.value)) {
          this.errorMessage = rule.message;
          this.hasError = true;
          this.$emit('validate', this.errorMessage);
          return;
        }
      }
      
      // 验证通过
      this.errorMessage = '';
      this.hasError = false;
      this.$emit('validate', '');
    }
  }
}
</script>

React实现:

// 父组件 RegistrationForm.js
import React, { useState } from 'react';
import ValidatedInput from './ValidatedInput';

function RegistrationForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: ''
  });
  
  const [validationErrors, setValidationErrors] = useState({
    username: '',
    email: '',
    password: ''
  });
  
  const [submitResult, setSubmitResult] = useState(null);

  const usernameRules = [
    { validator: value => value.length >= 3, message: '用户名至少3个字符' },
    { validator: value => /^[a-zA-Z0-9_]+$/.test(value), message: '用户名只能包含字母、数字和下划线' }
  ];

  const emailRules = [
    { validator: value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), message: '请输入有效的邮箱地址' }
  ];

  const passwordRules = [
    { validator: value => value.length >= 8, message: '密码至少8个字符' },
    { validator: value => /[A-Z]/.test(value), message: '密码必须包含大写字母' },
    { validator: value => /[0-9]/.test(value), message: '密码必须包含数字' }
  ];

  const updateField = (field, value) => {
    setFormData(prev => ({ ...prev, [field]: value }));
  };

  const handleValidation = (field, error) => {
    setValidationErrors(prev => ({ ...prev, [field]: error }));
  };

  const isFormValid = Object.values(validationErrors).every(error => !error);

  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (isFormValid) {
      setSubmitResult({
        success: true,
        data: { ...formData },
        timestamp: new Date().toISOString()
      });
      console.log('表单提交成功:', formData);
    } else {
      setSubmitResult({
        success: false,
        errors: { ...validationErrors }
      });
      console.log('表单验证失败:', validationErrors);
    }
  };

  return (
    <div className="registration-form">
      <h2>用户注册</h2>
      <form onSubmit={handleSubmit}>
        <ValidatedInput
          label="用户名"
          type="text"
          value={formData.username}
          onInput={(value) => updateField('username', value)}
          onValidate={(error) => handleValidation('username', error)}
          rules={usernameRules}
        />
        
        <ValidatedInput
          label="邮箱"
          type="email"
          value={formData.email}
          onInput={(value) => updateField('email', value)}
          onValidate={(error) => handleValidation('email', error)}
          rules={emailRules}
        />
        
        <ValidatedInput
          label="密码"
          type="password"
          value={formData.password}
          onInput={(value) => updateField('password', value)}
          onValidate={(error) => handleValidation('password', error)}
          rules={passwordRules}
        />
        
        <button type="submit" disabled={!isFormValid}>注册</button>
      </form>
      
      {submitResult && (
        <div className="result">
          <h3>提交结果</h3>
          <pre>{JSON.stringify(submitResult, null, 2)}</pre>
        </div>
      )}
    </div>
  );
}

export default RegistrationForm;
// 子组件 ValidatedInput.js
import React, { useState } from 'react';

function ValidatedInput({ label, type = 'text', value, onInput, onValidate, rules = [] }) {
  const [errorMessage, setErrorMessage] = useState('');
  const [hasError, setHasError] = useState(false);

  const handleInput = (e) => {
    const newValue = e.target.value;
    if (onInput) {
      onInput(newValue);
    }
    // 每次输入都清除错误状态
    setErrorMessage('');
    setHasError(false);
    if (onValidate) {
      onValidate('');
    }
  };

  const validate = () => {
    if (!value && rules.length > 0) {
      // 如果是必填项但为空,显示第一条规则的错误信息
      setErrorMessage(rules[0].message);
      setHasError(true);
      if (onValidate) {
        onValidate(rules[0].message);
      }
      return;
    }
    
    for (const rule of rules) {
      if (!rule.validator(value)) {
        setErrorMessage(rule.message);
        setHasError(true);
        if (onValidate) {
          onValidate(rule.message);
        }
        return;
      }
    }
    
    // 验证通过
    setErrorMessage('');
    setHasError(false);
    if (onValidate) {
      onValidate('');
    }
  };

  return (
    <div className="validated-input">
      <label>{label}</label>
      <input
        type={type}
        value={value}
        onChange={handleInput}
        onBlur={validate}
        className={hasError ? 'error' : ''}
      />
      {hasError && <div className="error-message">{errorMessage}</div>}
    </div>
  );
}

export default ValidatedInput;

三、高级篇:复杂场景与最佳实践

3.1 使用Ref进行DOM操作

有时候,我们需要在父组件中直接操作子组件的DOM或调用子组件的方法。这可以通过ref实现。

Vue示例:

<!-- 父组件 Parent.vue -->
<template>
  <div>
    <h1>父组件</h1>
    <ChildComponent ref="childRef" />
    <button @click="focusChildInput">聚焦子组件输入框</button>
    <button @click="callChildMethod">调用子组件方法</button>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  components: { ChildComponent },
  methods: {
    focusChildInput() {
      // 通过ref访问子组件的DOM元素
      const input = this.$refs.childRef.$refs.input;
      if (input) {
        input.focus();
      }
    },
    callChildMethod() {
      // 通过ref调用子组件的方法
      if (this.$refs.childRef) {
        const result = this.$refs.childRef.someMethod('参数');
        console.log('子组件方法返回:', result);
      }
    }
  }
}
</script>
<!-- 子组件 ChildComponent.vue -->
<template>
  <div>
    <h2>子组件</h2>
    <input ref="input" type="text" placeholder="输入框" />
    <button @click="someMethod">子组件方法</button>
  </div>
</template>

<script>
export default {
  methods: {
    someMethod(param) {
      console.log('子组件方法被调用,参数:', param);
      return `子组件处理了:${param}`;
    }
  }
}
</script>

React示例:

// 父组件 Parent.js
import React, { useRef } from 'react';
import ChildComponent from './ChildComponent';

function Parent() {
  const childRef = useRef();

  const focusChildInput = () => {
    // 通过ref访问子组件的DOM元素
    if (childRef.current && childRef.current.inputRef) {
      childRef.current.inputRef.current.focus();
    }
  };

  const callChildMethod = () => {
    // 通过ref调用子组件的方法
    if (childRef.current) {
      const result = childRef.current.someMethod('参数');
      console.log('子组件方法返回:', result);
    }
  };

  return (
    <div>
      <h1>父组件</h1>
      <ChildComponent ref={childRef} />
      <button onClick={focusChildInput}>聚焦子组件输入框</button>
      <button onClick={callChildMethod}>调用子组件方法</button>
    </div>
  );
}

export default Parent;
// 子组件 ChildComponent.js
import React, { useRef, useImperativeHandle, forwardRef } from 'react';

const ChildComponent = forwardRef((props, ref) => {
  const inputRef = useRef();

  // 使用useImperativeHandle暴露特定的方法给父组件
  useImperativeHandle(ref, () => ({
    someMethod: (param) => {
      console.log('子组件方法被调用,参数:', param);
      return `子组件处理了:${param}`;
    },
    inputRef: inputRef
  }));

  return (
    <div>
      <h2>子组件</h2>
      <input ref={inputRef} type="text" placeholder="输入框" />
      <button onClick={() => console.log('子组件按钮点击')}>子组件按钮</button>
    </div>
  );
});

export default ChildComponent;

3.2 插槽(Slots)与Render Props

在某些场景下,我们可能需要将内容或逻辑传递给子组件,而不是简单的数据。这时插槽或Render Props就派上用场了。

Vue插槽示例:

<!-- 父组件 Parent.vue -->
<template>
  <div>
    <h1>父组件</h1>
    
    <!-- 默认插槽 -->
    <CardComponent>
      <p>这是通过默认插槽传递的内容</p>
      <button>按钮</button>
    </CardComponent>
    
    <!-- 具名插槽 -->
    <CardComponent>
      <template #header>
        <h3>卡片标题</h3>
      </template>
      <template #default>
        <p>这是主要内容区域</p>
      </template>
      <template #footer>
        <button>确认</button>
        <button>取消</button>
      </template>
    </CardComponent>
    
    <!-- 作用域插槽 -->
    <ListComponent :items="items">
      <template #default="{ item, index }">
        <div class="list-item">
          <span>{{ index + 1 }}. {{ item.name }}</span>
          <span>价格:¥{{ item.price }}</span>
        </div>
      </template>
    </ListComponent>
  </div>
</template>

<script>
import CardComponent from './CardComponent.vue'
import ListComponent from './ListComponent.vue'

export default {
  components: { CardComponent, ListComponent },
  data() {
    return {
      items: [
        { id: 1, name: '商品A', price: 100 },
        { id: 2, name: '商品B', price: 200 },
        { id: 3, name: '商品C', price: 300 }
      ]
    }
  }
}
</script>
<!-- 子组件 CardComponent.vue -->
<template>
  <div class="card">
    <div class="card-header" v-if="$slots.header">
      <slot name="header"></slot>
    </div>
    <div class="card-body">
      <slot></slot>
    </div>
    <div class="card-footer" v-if="$slots.footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>
<!-- 子组件 ListComponent.vue -->
<template>
  <div class="list">
    <div 
      v-for="(item, index) in items" 
      :key="item.id"
      class="list-item-wrapper"
    >
      <!-- 作用域插槽:子组件将item和index传递给父组件 -->
      <slot :item="item" :index="index"></slot>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    items: {
      type: Array,
      required: true
    }
  }
}
</script>

React Render Props示例:

// 父组件 Parent.js
import React from 'react';
import CardComponent from './CardComponent';
import ListComponent from './ListComponent';

function Parent() {
  const items = [
    { id: 1, name: '商品A', price: 100 },
    { id: 2, name: '商品B', price: 200 },
    { id: 3, name: '商品C', price: 300 }
  ];

  return (
    <div>
      <h1>父组件</h1>
      
      {/* Render Props示例 */}
      <CardComponent
        renderHeader={() => <h3>卡片标题</h3>}
        renderBody={() => <p>这是主要内容区域</p>}
        renderFooter={() => (
          <>
            <button>确认</button>
            <button>取消</button>
          </>
        )}
      />
      
      {/* 作用域插槽等价物 */}
      <ListComponent items={items}>
        {(item, index) => (
          <div className="list-item">
            <span>{index + 1}. {item.name}</span>
            <span>价格:¥{item.price}</span>
          </div>
        )}
      </ListComponent>
    </div>
  );
}

export default Parent;
// 子组件 CardComponent.js
import React from 'react';

function CardComponent({ renderHeader, renderBody, renderFooter }) {
  return (
    <div className="card">
      {renderHeader && (
        <div className="card-header">
          {renderHeader()}
        </div>
      )}
      <div className="card-body">
        {renderBody ? renderBody() : null}
      </div>
      {renderFooter && (
        <div className="card-footer">
          {renderFooter()}
        </div>
      )}
    </div>
  );
}

export default CardComponent;
// 子组件 ListComponent.js
import React from 'react';

function ListComponent({ items, children }) {
  return (
    <div className="list">
      {items.map((item, index) => (
        <div key={item.id} className="list-item-wrapper">
          {/* 调用children函数,将item和index传递给父组件 */}
          {children(item, index)}
        </div>
      ))}
    </div>
  );
}

export default ListComponent;

3.3 实战案例:可复用的模态框组件

模态框是一个典型的父子组件通信场景,需要父组件控制模态框的显示/隐藏,子组件处理内部逻辑并通知父组件。

Vue实现:

<!-- 父组件 Parent.vue -->
<template>
  <div>
    <h1>模态框示例</h1>
    <button @click="showModal = true">打开模态框</button>
    
    <ModalComponent 
      v-model="showModal"
      title="用户信息"
      :width="500"
      @confirm="handleConfirm"
      @cancel="handleCancel"
    >
      <template #content>
        <form @submit.prevent="handleSubmit">
          <div class="form-group">
            <label>姓名:</label>
            <input v-model="form.name" type="text" required />
          </div>
          <div class="form-group">
            <label>邮箱:</label>
            <input v-model="form.email" type="email" required />
          </div>
          <div class="form-group">
            <label>年龄:</label>
            <input v-model="form.age" type="number" min="1" max="120" />
          </div>
        </form>
      </template>
    </ModalComponent>
    
    <div v-if="submittedData" class="result">
      <h3>提交的数据:</h3>
      <pre>{{ JSON.stringify(submittedData, null, 2) }}</pre>
    </div>
  </div>
</template>

<script>
import ModalComponent from './ModalComponent.vue'

export default {
  components: { ModalComponent },
  data() {
    return {
      showModal: false,
      form: {
        name: '',
        email: '',
        age: ''
      },
      submittedData: null
    }
  },
  methods: {
    handleConfirm() {
      // 验证表单
      if (!this.form.name || !this.form.email) {
        alert('请填写必填字段');
        return;
      }
      
      // 提交数据
      this.submittedData = { ...this.form };
      console.log('模态框确认,数据:', this.form);
      
      // 关闭模态框
      this.showModal = false;
      
      // 重置表单
      this.form = { name: '', email: '', age: '' };
    },
    handleCancel() {
      console.log('模态框取消');
      // 重置表单
      this.form = { name: '', email: '', age: '' };
    },
    handleSubmit() {
      // 表单提交事件,可以在这里处理
      console.log('表单提交');
    }
  }
}
</script>
<!-- 子组件 ModalComponent.vue -->
<template>
  <div v-if="visible" class="modal-overlay" @click="handleOverlayClick">
    <div class="modal" :style="{ width: width + 'px' }" @click.stop>
      <div class="modal-header">
        <h3>{{ title }}</h3>
        <button class="close-btn" @click="close">×</button>
      </div>
      <div class="modal-body">
        <slot name="content"></slot>
      </div>
      <div class="modal-footer">
        <button @click="handleCancel">取消</button>
        <button @click="handleConfirm" class="confirm-btn">确认</button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    modelValue: {
      type: Boolean,
      default: false
    },
    title: {
      type: String,
      default: '模态框'
    },
    width: {
      type: Number,
      default: 400
    }
  },
  computed: {
    visible() {
      return this.modelValue;
    }
  },
  methods: {
    close() {
      this.$emit('update:modelValue', false);
    },
    handleOverlayClick() {
      // 点击遮罩层关闭
      this.close();
    },
    handleConfirm() {
      this.$emit('confirm');
    },
    handleCancel() {
      this.close();
      this.$emit('cancel');
    }
  }
}
</script>

React实现:

// 父组件 Parent.js
import React, { useState } from 'react';
import ModalComponent from './ModalComponent';

function Parent() {
  const [showModal, setShowModal] = useState(false);
  const [form, setForm] = useState({
    name: '',
    email: '',
    age: ''
  });
  const [submittedData, setSubmittedData] = useState(null);

  const handleConfirm = () => {
    // 验证表单
    if (!form.name || !form.email) {
      alert('请填写必填字段');
      return;
    }
    
    // 提交数据
    setSubmittedData({ ...form });
    console.log('模态框确认,数据:', form);
    
    // 关闭模态框
    setShowModal(false);
    
    // 重置表单
    setForm({ name: '', email: '', age: '' });
  };

  const handleCancel = () => {
    console.log('模态框取消');
    // 重置表单
    setForm({ name: '', email: '', age: '' });
  };

  const updateForm = (field, value) => {
    setForm(prev => ({ ...prev, [field]: value }));
  };

  return (
    <div>
      <h1>模态框示例</h1>
      <button onClick={() => setShowModal(true)}>打开模态框</button>
      
      <ModalComponent
        visible={showModal}
        onClose={() => setShowModal(false)}
        title="用户信息"
        width={500}
        onConfirm={handleConfirm}
        onCancel={handleCancel}
      >
        <form onSubmit={(e) => e.preventDefault()}>
          <div className="form-group">
            <label>姓名:</label>
            <input 
              value={form.name}
              onChange={(e) => updateForm('name', e.target.value)}
              type="text" 
              required 
            />
          </div>
          <div className="form-group">
            <label>邮箱:</label>
            <input 
              value={form.email}
              onChange={(e) => updateForm('email', e.target.value)}
              type="email" 
              required 
            />
          </div>
          <div className="form-group">
            <label>年龄:</label>
            <input 
              value={form.age}
              onChange={(e) => updateForm('age', e.target.value)}
              type="number" 
              min="1" 
              max="120" 
            />
          </div>
        </form>
      </ModalComponent>
      
      {submittedData && (
        <div className="result">
          <h3>提交的数据:</h3>
          <pre>{JSON.stringify(submittedData, null, 2)}</pre>
        </div>
      )}
    </div>
  );
}

export default Parent;
// 子组件 ModalComponent.js
import React, { useEffect } from 'react';

function ModalComponent({ 
  visible, 
  onClose, 
  title = '模态框', 
  width = 400, 
  onConfirm, 
  onCancel, 
  children 
}) {
  // 处理ESC键关闭
  useEffect(() => {
    const handleKeyDown = (e) => {
      if (e.key === 'Escape' && visible) {
        onClose();
      }
    };
    
    if (visible) {
      document.addEventListener('keydown', handleKeyDown);
    }
    
    return () => {
      document.removeEventListener('keydown', handleKeyDown);
    };
  }, [visible, onClose]);

  const handleOverlayClick = (e) => {
    if (e.target === e.currentTarget) {
      onClose();
    }
  };

  const handleConfirm = () => {
    if (onConfirm) {
      onConfirm();
    }
  };

  const handleCancel = () => {
    if (onCancel) {
      onCancel();
    }
    onClose();
  };

  if (!visible) {
    return null;
  }

  return (
    <div className="modal-overlay" onClick={handleOverlayClick}>
      <div className="modal" style={{ width: `${width}px` }}>
        <div className="modal-header">
          <h3>{title}</h3>
          <button className="close-btn" onClick={onClose}>×</button>
        </div>
        <div className="modal-body">
          {children}
        </div>
        <div className="modal-footer">
          <button onClick={handleCancel}>取消</button>
          <button onClick={handleConfirm} className="confirm-btn">确认</button>
        </div>
      </div>
    </div>
  );
}

export default ModalComponent;

四、最佳实践与性能优化

4.1 避免不必要的重新渲染

Vue优化:

<!-- 使用v-once缓存静态内容 -->
<ChildComponent v-once :static-data="staticData" />

<!-- 使用计算属性避免重复计算 -->
<script>
export default {
  computed: {
    // 缓存计算结果,只有依赖变化时才重新计算
    processedData() {
      return this.rawData.map(item => ({
        ...item,
        processed: this.expensiveProcessing(item)
      }));
    }
  }
}
</script>

React优化:

// 使用React.memo避免不必要的重新渲染
import React, { memo } from 'react';

const ChildComponent = memo(({ data, onAction }) => {
  console.log('ChildComponent渲染');
  return (
    <div>
      <p>数据:{data}</p>
      <button onClick={onAction}>操作</button>
    </div>
  );
});

// 使用useCallback和useMemo
function Parent() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState('初始数据');

  // 使用useCallback缓存函数引用
  const handleAction = useCallback(() => {
    console.log('执行操作');
  }, []);

  // 使用useMemo缓存计算结果
  const processedData = useMemo(() => {
    return data.toUpperCase();
  }, [data]);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        重新渲染父组件 ({count})
      </button>
      <ChildComponent data={processedData} onAction={handleAction} />
    </div>
  );
}

4.2 合理使用Props验证

// Vue - 详细的props验证
props: {
  // 基础类型
  id: [String, Number],
  
  // 对象类型,指定结构
  user: {
    type: Object,
    default: () => ({}),
    validator: (user) => {
      return user.name && user.email;
    }
  },
  
  // 数组类型,指定元素类型
  items: {
    type: Array,
    default: () => [],
    validator: (items) => {
      return items.every(item => typeof item.id === 'number');
    }
  }
}

// React - 使用PropTypes进行运行时检查
import PropTypes from 'prop-types';

ChildComponent.propTypes = {
  id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  user: PropTypes.shape({
    name: PropTypes.string.isRequired,
    email: PropTypes.string.isRequired,
    age: PropTypes.number
  }),
  items: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number.isRequired,
      name: PropTypes.string
    })
  )
};

4.3 组件通信的边界

何时使用Props/Events:

  • 父子组件直接通信
  • 数据流向清晰
  • 组件层级较浅

何时考虑其他方案:

  • 跨多层组件通信(使用Provide/Inject或Context)
  • 全局状态管理(Vuex/Redux)
  • 非父子关系的组件通信(事件总线)

五、总结

父子组件通信是前端组件化开发的基础。通过本文的学习,你应该掌握了:

  1. 基础通信:Props传递数据,Events实现子组件向父组件通信
  2. 进阶技巧:Ref操作、插槽/Render Props的使用
  3. 复杂场景:表单验证、模态框等实战案例
  4. 最佳实践:性能优化、Props验证、通信边界

记住,良好的组件通信设计应该遵循以下原则:

  • 单一职责:每个组件只做一件事
  • 数据单向流动:避免双向绑定导致的数据流混乱
  • 明确接口:通过Props和Events定义清晰的组件接口
  • 可复用性:设计可复用的组件,减少重复代码

在实际项目中,根据具体需求选择合适的通信方式。对于简单场景,Props/Events足够;对于复杂应用,可能需要结合状态管理工具。不断实践和优化,你的组件设计能力会越来越强。