Props 传递数据

Props 是父组件向子组件传递数据的标准方式。它让组件可配置、可复用,是组件化开发的基础。

理解 Props 的各种用法和注意事项,能帮你设计出更好的组件接口。

基本用法

数组语法

最简单的 props 定义方式:

Vue.component('user-card', {
  props: ['name', 'age', 'email'],
  template: `
    <div class="user-card">
      <h3>{{ name }}</h3>
      <p>年龄:{{ age }}</p>
      <p>邮箱:{{ email }}</p>
    </div>
  `
})
<user-card 
  name="张三" 
  age="25" 
  email="zhangsan@example.com"
></user-card>

对象语法

推荐使用对象语法,可以指定类型:

Vue.component('user-card', {
  props: {
    name: String,
    age: Number,
    email: String
  },
  template: `...`
})

传递数据的方式

静态传递

直接传递字符串:

<user-card name="张三"></user-card>

动态传递

使用 v-bind 传递动态数据:

<user-card :name="userName"></user-card>
<user-card :user="currentUser"></user-card>

传递数字

<!-- 静态传递是字符串 -->
<user-card age="25"></user-card>  <!-- "25" 字符串 -->

<!-- 使用 v-bind 传递数字 -->
<user-card :age="25"></user-card>  <!-- 25 数字 -->

传递布尔值

<!-- 传递 true -->
<my-component :active="true"></my-component>

<!-- 简写:包含 props 默认为 true -->
<my-component active></my-component>

<!-- 传递 false -->
<my-component :active="false"></my-component>

传递数组

<user-list :users="['张三', '李四']"></user-list>

传递对象

<user-card :user="{ name: '张三', age: 25 }"></user-card>

传递对象属性

<user-card v-bind="user"></user-card>

<!-- 等同于 -->
<user-card 
  :name="user.name"
  :age="user.age"
  :email="user.email"
></user-card>

Props 验证

类型验证

指定 prop 的类型:

props: {
  name: String,
  age: Number,
  active: Boolean,
  tags: Array,
  user: Object,
  callback: Function,
  promise: Promise
}

多类型

允许 prop 是多种类型之一:

props: {
  value: [String, Number],
  visible: [Boolean, Number]
}

必填验证

props: {
  name: {
    type: String,
    required: true
  }
}

默认值

props: {
  type: {
    type: String,
    default: 'primary'
  },
  size: {
    type: Number,
    default: 14
  }
}

对象/数组默认值

对象和数组的默认值必须使用工厂函数:

props: {
  user: {
    type: Object,
    default: function() {
      return { name: '匿名' }
    }
  },
  tags: {
    type: Array,
    default: function() {
      return []
    }
  }
}

自定义验证

props: {
  status: {
    validator: function(value) {
      return ['active', 'inactive', 'pending'].indexOf(value) !== -1
    }
  },
  age: {
    validator: function(value) {
      return value >= 0 && value <= 150
    }
  }
}

完整验证示例

Vue.component('user-card', {
  props: {
    name: {
      type: String,
      required: true
    },
    age: {
      type: Number,
      default: 18,
      validator: function(value) {
        return value >= 0 && value <= 150
      }
    },
    email: {
      type: String,
      default: ''
    },
    role: {
      type: String,
      default: 'user',
      validator: function(value) {
        return ['admin', 'user', 'guest'].indexOf(value) !== -1
      }
    },
    tags: {
      type: Array,
      default: function() {
        return []
      }
    }
  },
  template: `
    <div class="user-card">
      <h3>{{ name }}</h3>
      <p>年龄:{{ age }}</p>
      <p>邮箱:{{ email }}</p>
      <p>角色:{{ role }}</p>
      <p>标签:{{ tags.join(', ') }}</p>
    </div>
  `
})

单向数据流

Props 是单向绑定的:父组件数据变化会传递给子组件,但子组件不应该修改 props。

不要修改 Props

// ❌ 错误:直接修改 props
props: ['message'],
methods: {
  update() {
    this.message = 'new value'  // Vue 会警告
  }
}

处理方式

1. 使用本地数据

props: ['initialCount'],
data() {
  return {
    count: this.initialCount
  }
}

2. 使用计算属性

props: ['size'],
computed: {
  normalizedSize() {
    return this.size.trim().toLowerCase()
  }
}

3. 使用事件通知父组件

props: ['value'],
methods: {
  updateValue(newValue) {
    this.$emit('input', newValue)
  }
}

Props 大小写

HTML 不区分大小写,所以 props 在模板中要使用短横线命名:

// JavaScript 中使用驼峰命名
props: {
  userName: String,
  isActive: Boolean
}
<!-- HTML 中使用短横线命名 -->
<my-component user-name="张三" is-active></my-component>

非 Prop 属性

组件可以接收未在 props 中定义的属性,这些属性会自动添加到组件根元素上。

自动继承

<my-component class="custom-class" style="color: red" data-id="123"></my-component>

classstyledata-id 会自动添加到组件根元素。

替换与合并

Vue.component('my-input', {
  template: '<input type="text" class="form-control">'
})
<!-- class 会合并 -->
<my-input class="custom-input"></my-input>
<!-- 结果:<input type="text" class="form-control custom-input"> -->

<!-- type 会替换 -->
<my-input type="password"></my-input>
<!-- 结果:<input type="password" class="form-control"> -->

禁用属性继承

Vue.component('my-component', {
  inheritAttrs: false,
  template: `
    <div>
      <input v-bind="$attrs">
    </div>
  `
})

实战示例

可配置的按钮组件

Vue.component('app-button', {
  props: {
    type: {
      type: String,
      default: 'primary',
      validator: function(value) {
        return ['primary', 'secondary', 'danger', 'success'].indexOf(value) !== -1
      }
    },
    size: {
      type: String,
      default: 'medium',
      validator: function(value) {
        return ['small', 'medium', 'large'].indexOf(value) !== -1
      }
    },
    disabled: {
      type: Boolean,
      default: false
    },
    loading: {
      type: Boolean,
      default: false
    }
  },
  template: `
    <button 
      :class="['btn', 'btn-' + type, 'btn-' + size]"
      :disabled="disabled || loading"
    >
      <span v-if="loading" class="spinner"></span>
      <slot></slot>
    </button>
  `
})
<app-button type="primary" size="large">提交</app-button>
<app-button type="danger" :loading="isLoading">删除</app-button>
<app-button type="secondary" disabled>禁用</app-button>

可配置的输入框组件

Vue.component('app-input', {
  props: {
    value: [String, Number],
    type: {
      type: String,
      default: 'text'
    },
    placeholder: String,
    disabled: Boolean,
    readonly: Boolean,
    maxlength: Number,
    label: String,
    error: String
  },
  template: `
    <div class="form-group">
      <label v-if="label">{{ label }}</label>
      <input 
        :type="type"
        :value="value"
        :placeholder="placeholder"
        :disabled="disabled"
        :readonly="readonly"
        :maxlength="maxlength"
        @input="$emit('input', $event.target.value)"
        @change="$emit('change', $event)"
        @focus="$emit('focus', $event)"
        @blur="$emit('blur', $event)"
      >
      <span v-if="error" class="error">{{ error }}</span>
    </div>
  `
})

列表组件

Vue.component('data-list', {
  props: {
    items: {
      type: Array,
      required: true
    },
    loading: {
      type: Boolean,
      default: false
    },
    emptyText: {
      type: String,
      default: '暂无数据'
    },
    itemKey: {
      type: String,
      default: 'id'
    }
  },
  template: `
    <div class="data-list">
      <div v-if="loading" class="loading">加载中...</div>
      <div v-else-if="items.length === 0" class="empty">{{ emptyText }}</div>
      <ul v-else class="list">
        <li v-for="item in items" :key="item[itemKey]">
          <slot :item="item">{{ item }}</slot>
        </li>
      </ul>
    </div>
  `
})

注意事项

Props 验证时机

Props 验证在组件实例创建之前进行,所以 defaultvalidator 函数中不能访问组件实例(this)。

// ❌ 错误:validator 中访问 this
props: {
  value: {
    validator(val) {
      return this.someCondition  // this 是 undefined
    }
  }
}

// ✅ 正确:使用纯函数
props: {
  value: {
    validator(val) {
      return val >= 0  // 不依赖 this
    }
  }
}

引用类型 Props

对象和数组是引用类型,子组件修改会影响父组件:

props: ['user'],
methods: {
  changeName() {
    this.user.name = '新名字'  // 会影响父组件
  }
}

虽然 Vue 不会警告,但这违反了单向数据流原则。应该避免直接修改引用类型的属性。

小结

Props 要点:

  1. 单向数据流:props 只能从父组件流向子组件
  2. 类型验证:使用对象语法定义 props,添加类型验证
  3. 默认值:对象和数组的默认值要用工厂函数
  4. 不要修改:子组件不应该直接修改 props