Code前端首页关于Code前端联系我们

Vue2里的props怎么用?从声明到传值避坑全解析

terry 8小时前 阅读数 13 #Vue
文章标签 Vue2 props

不少刚开始学Vue2的同学,一碰到props就犯愁——到底咋声明?父组件咋传数据?传的时候类型不对咋办?还有单向数据流是啥意思?别慌,这篇把props从基础到进阶的知识点拆成问答,帮你把每个环节吃透~

props到底是干啥的?

props是Vue里父组件给子组件传数据的核心方式,你可以理解成“子组件对外暴露的‘接口’”,父组件通过这个接口给子组件塞数据,让同一个子组件在不同场景下显示不同内容。

举个实际例子:你做了个「商品卡片组件」,首页、分类页都要用它,这时候父组件(首页)把商品名称、价格这些数据通过props传给子组件(商品卡片),子组件只负责渲染,这样一个组件就能在不同页面复用,数据由父组件控制,是不是很灵活?

怎么在子组件里声明props?

有两种声明风格:简单数组式对象验证式,场景不同用法也不同~

数组式(快速声明,无验证)

如果只是简单传值,不需要验证类型、必填这些,直接用数组列出来要接收的prop名称:

Vue.component('Child', {
  props: ['name', 'age', 'isVip']
})

这种写法轻快,但缺点是没法做任何验证,适合小项目或临时组件。

对象式(带验证,严谨开发必用)

当你需要控制prop的类型、是否必填、默认值、自定义验证逻辑时,得用对象格式,每个prop对应一个配置项,常见配置有这几个:

  • type:指定prop的类型,比如StringNumberBooleanArrayObject,甚至自定义构造函数(比如自己写的class);
  • required:布尔值,标记这个prop是否必须由父组件传入;
  • default:当父组件没传这个prop时,用啥默认值(注意:对象/数组的default要写成函数返回值,后面讲原因);
  • validator:自定义验证函数,传入prop的值,返回布尔值判断是否合法。

举个完整例子,做一个「用户信息组件」,要求name必填(字符串)、age选填(数字,默认18)、tags必须是数组且长度≥1:

Vue.component('UserCard', {
  props: {
    name: {
      type: String,
      required: true // 父组件必须传name
    },
    age: {
      type: Number,
      default: 18 // 没传就用18
    },
    tags: {
      type: Array,
      // 数组/对象的default要返回函数,避免所有实例共享同一个引用
      default: () => [], 
      validator: (value) => { // 验证tags长度
        return value.length >= 1
      }
    }
  }
})

这样写后,父组件传值不对时,开发环境会直接在控制台报错提醒,相当于给代码加了“安全锁”~

父组件怎么给子组件传props?

父传子的方式分静态传递动态传递,还有传对象/数组的技巧,一个个说:

静态传递(传固定值)

如果传给子组件的是“死数据”(比如固定字符串、数字),直接在标签上写属性:

<Child name="张三" age="18" />

注意哦!这种写法下,值的类型是字符串!哪怕你写age="18",子组件里如果声明typeNumber,这里传的还是字符串'18',所以静态传递只适合字符串类型,或者子组件不验证类型的场景。

动态传递(传变量/表达式)

如果要传父组件的变量、布尔值、数组、对象,或者控制类型,得用v-bind(简写)绑定JS表达式:

<!-- 传变量 -->
<Child :name="parentName" :age="parentAge" />
<!-- 传布尔值(加:`才是布尔true,否则是字符串'true') -->
<Child :isVip="true" />
<!-- 传表达式 -->
<Child :score="Math.random() * 100" />

用绑定后,值的类型由JS表达式决定(比如parentAge是数字,传过去就是数字;Math.random()返回数字,传过去也是数字),这样才能和子组件的type验证对应上~

批量传对象/数组

如果父组件有个对象,里面包含子组件需要的多个props,不用一个个传,直接用v-bind="对象"批量传递:

// 父组件data里的对象
data() {
  return {
    userInfo: {
      name: '李四',
      age: 22,
      isVip: false
    }
  }
}
<!-- 子组件需要name、age、isVip三个props -->
<Child v-bind="userInfo" />
<!-- 等价于分别传:
<Child :name="userInfo.name" :age="userInfo.age" :isVip="userInfo.isVip" />
-->

这种写法在对象属性多的时候超省心,代码也更简洁~

props的验证规则怎么玩?

前面提了typerequireddefaultvalidator这些配置,这里展开讲讲细节,避免踩坑:

type可以玩出花

type不止支持内置类型(String/Number等),还能:

  • 多个类型:比如type: [String, Number],表示这个prop可以是字符串或数字;
  • 自定义构造函数:比如你写了个class Persontype: Person,父组件传new Person()进来才会验证通过;

举个自定义构造函数的例子:

// 定义Person类
class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
}
// 子组件props验证
props: {
  user: {
    type: Person, // 只有传Person实例才合法
    required: true
  }
}
// 父组件传值
<Child :user="new Person('王五', 25)" />

default的“坑”:对象/数组必须用函数

如果prop的类型是ObjectArray,直接写default: {name: '默认'} 会出问题——所有子组件实例会共享同一个对象/数组引用!比如一个子组件改了这个对象的属性,其他子组件的默认值也会被改掉,这是引用类型的特性导致的。

所以正确写法是用函数返回默认值,每个实例都能拿到独立的副本:

props: {
  settings: {
    type: Object,
    // 错误写法:default: { theme: 'light' } 
    // 正确写法:
    default: () => ({ theme: 'light' })
  },
  hobbies: {
    type: Array,
    default: () => []
  }
}

validator:自定义“安检员”

validator是个函数,接收当前prop的值作为参数,返回true/false表示是否合法,比如验证手机号格式:

props: {
  phone: {
    type: String,
    validator: (value) => {
      // 正则判断是否是11位手机号
      return /^1\d{10}$/.test(value)
    }
  }
}

如果父组件传了个不符合规则的手机号,开发环境控制台会直接报错,相当于给数据加了“格式过滤器”~

props的单向数据流是啥?为啥不能直接改props?

Vue里props遵循“单向数据流”规则:父组件传值给子组件后,子组件不能直接修改props的值,为啥要这么设计?

想象一下:如果子组件能直接改props,父组件里的数据源就会被偷偷修改,整个项目的数据流向会变得混乱,调试时根本不知道谁改了数据,所以Vue强制让数据“父→子”单向流动,保证数据可预测。

那子组件要改props的值咋办?分两种场景:

只是“用”props的值,但需要局部修改

把props的值复制到子组件的data里,后续改data里的副本:

Vue.component('Child', {
  props: ['count'],
  data() {
    return {
      localCount: this.count // 复制props到data
    }
  },
  methods: {
    increment() {
      this.localCount++ // 改的是localCount,不影响父组件
    }
  }
})

需要把修改同步回父组件

子组件通过$emit触发父组件的事件,让父组件自己修改数据源,从而更新子组件的props,比如子组件有个“+1”按钮,要让父组件的count加1:

// 子组件
Vue.component('Child', {
  props: ['count'],
  methods: {
    handleClick() {
      this.$emit('update-count', this.count + 1) // 触发事件,传新值
    }
  }
})
// 父组件
<template>
  <div>
    <Child :count="parentCount" @update-count="parentCount = $event" />
  </div>
</template>
<script>
export default {
  data() {
    return {
      parentCount: 0
    }
  }
}
</script>

这样父组件主动修改自己的parentCount,子组件的props会自动更新(因为单向数据流,父变了子跟着变),既遵守规则又实现了交互~

props和data、computed有啥区别?

很多同学刚学的时候会混淆这三个,其实它们的“身份”和作用域完全不同:

特性 props data computed
数据来源 父组件传入(外部) 组件内部自己定义(内部) 基于props、data等计算衍生(内部)
可修改性 不能直接改(单向流) 可以自由修改(内部状态) 一般是只读(由依赖决定)
典型场景 子组件接收父组件配置 组件内部的临时状态(比如表单输入) 复杂逻辑的封装(比如购物车总价计算)

举个生活例子:你点外卖,props像“商家给你的餐品”(外部给的),data像“你自己加的辣椒醋”(内部调的),computed像“算出这顿饭总共花多少钱”(基于餐品和调料计算的结果)~

传值时类型不匹配咋办?

最常见的坑是父组件传值类型和子组件type声明不一致,比如父传字符串"18",子组件要Number类型的age,这时候得注意传值方式:

  • 如果是静态传递(没加),比如<Child age="18" />,子组件拿到的是字符串'18',哪怕type声明Number也没用;
  • 如果是动态传递(加),比如<Child :age="18" />,子组件拿到的是数字18,这时候type验证才能生效。

所以记住:想传非字符串类型,必须用v-bind绑定JS表达式!比如传布尔值true,得写:isVip="true",不然传的是字符串'true';传数组/对象同理,必须用绑定。

子组件能主动通知父组件更新props吗?

前面讲单向数据流时提过,子组件不能直接改props,但可以通过$emit事件让父组件自己改,流程是:

  1. 子组件触发$emit('事件名', 新值)
  2. 父组件监听这个事件,修改自己的数据源(比如data里的变量)
  3. 因为父组件的数据源变了,子组件的props会自动更新(单向流的“父变子变”)

举个实时搜索的例子:子组件是搜索输入框,输入内容后要让父组件的searchKey更新,从而重新请求数据,代码如下:

// 子组件(搜索框)
Vue.component('SearchInput', {
  props: ['value'], // 父组件传的当前搜索关键词
  methods: {
    handleInput(e) {
      const newKey = e.target.value
      this.$emit('input', newKey) // 触发input事件,传新关键词
    }
  },
  template: `<input :value="value" @input="handleInput" />`
})
// 父组件
<template>
  <div>
    <SearchInput :value="searchKey" @input="searchKey = $event" />
    <button @click="fetchData">搜索</button>
  </div>
</template>
<script>
export default {
  data() {
    return {
      searchKey: ''
    }
  },
  methods: {
    fetchData() {
      // 用searchKey发请求...
    }
  }
}
</script>

这样子组件输入时,通过$emit通知父组件改searchKey,父组件的searchKey变了,子组件的value(props)也会跟着变,完美绕开“直接改props”的坑~

props的驼峰命名和短横线命名咋处理?

HTML标签的属性名是不区分大小写的,所以Vue里有个“驼峰→短横线”的转换规则:

  • 子组件props声明用驼峰命名(比如userName);
  • 父组件模板中传值时,要用短横线命名(比如user-name);
  • 但如果是用JS动态绑定(:userName),因为在JS上下文里,驼峰是合法的,所以也能传。

举个例子:

// 子组件声明
props: ['userName']
// 父组件模板里的两种传法:
<!-- 静态传,必须用短横线 -->
<Child user-name="赵六" />
<!-- 动态传,驼峰或短横线都能识别,但建议和子组件声明一致 -->
<Child :userName="name" /> 
<Child :user-name="name" /> 

简单说:模板里静态写属性名用短横线,动态绑定()时驼峰/短横线都行,但为了统一,建议和子组件props的命名风格一致~

props验证不通过会怎样?

开发环境下,Vue会帮你“挑错”:如果prop的类型不对、required没传、validator返回false,控制台会弹出警告,直接告诉你哪里传错了,相当于有个“代码检查助手”;

但到了生产环境(打包后),为了性能,Vue会把props验证的代码删掉,所以验证只在开发时起作用,用来提前发现错误,避免线上bug~

看到这,你对Vue2的props应该从“一头雾水”变成“心里有数”了吧?记住核心逻辑:props是父传子的桥梁,声明时做好验证,传值时注意类型和绑定方式,修改时遵守单向数据流规则,多写几个组件练手,这些知识点自然就吃透啦~如果还有疑问,评论区随时喊我~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门