Skip to content

为什么要将data设置为函数与父向子传递参数-属性(Lesson4 3/3) ​

本页对应 Lesson04,共 5 段视频,约 1 小时 34 分。

学习目标 ​

  • 能够说明组件中 data 为什么必须写成返回对象的函数,并能在示例中正确声明组件局部状态。
  • 能够使用 props 接收父组件数据,并通过 type、required、default 限制类型、必填项与默认值,且能为对象/数组默认值正确使用工厂函数。
  • 能够编写父传子示例,通过属性传递列表、标题、颜色等数据,并在子组件模板中完成渲染。
  • 能够通过向子组件传递父组件函数,在子组件中调用该函数并修改父组件状态。
  • 能够使用 $emit 触发自定义事件,在父组件绑定事件处理函数,并正确完成事件参数传递。
  • 能够使用 .sync 修饰符实现属性同步,并在子组件中通过 $emit('update:属性名', 新值) 更新父组件数据。

知识点 ​

先看本节代码要验证什么 ​

本节要解决一个组件开发中非常常见的问题:为什么组件的 data 要写成函数,而不是直接写一个对象?
示例里放了两个组件:App 和 BiaoGe。App 的 data 是函数,每次返回新的 { num: 1 };BiaoGe 的 data 也写成函数,但它返回的是外部同一个 obj。这样就能直观看到“数据独立”和“数据共享”的差别。

本节示例代码:

html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Title</title>
	<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
	<app></app>
	<app></app>
	<app></app>
	<app></app>
	<app></app>
	<app></app>
	<hr/>
	<biao-ge></biao-ge>
	<biao-ge></biao-ge>
	<biao-ge></biao-ge>
	<biao-ge></biao-ge>
	<biao-ge></biao-ge>
	<biao-ge></biao-ge>
</div>
</body>
<script>
	// 1- 函数必须返回单例模式的对象,可以让组件中的数据独立
	// 2- 可以设置逻辑程序
	let obj = {
		num:1
	}
	new Vue({
		el:"#app",
		components:{
			App:{
				data(){
					console.log("data");
					return {
						num:1
					}
				},
				template:(`
					<button @click="changeNum">{{num}}</button>
				`),
				methods:{
					changeNum(){
						this.num++
					}
				}
			},
			BiaoGe:{
				data(){
					return obj
				},
				template:(`
					<button @click="changeNum">{{num}}</button>
				`),
				methods:{
					changeNum(){
						this.num++;
					}
				}
			}
		}
	})
</script>
</html>

为什么组件的 data 必须是函数 ​

为什么需要: 组件不是只渲染一次就结束的。像示例里 <app></app> 写了 6 次,Vue 会创建 6 个组件实例。如果 data 直接写成一个对象,那么这 6 个实例很可能拿到同一份数据;点击其中一个按钮,其他按钮也会跟着变。
怎么想: 把 data 想成“生产数据的工厂函数”。转写稿里讲工厂函数时提到:工厂函数就是一个函数,调用它,它返回一个字面量对象;每次调用都可以产生一份新的对象。组件的 data 也是这个思路:每次创建组件实例时调用一次,返回一份新对象。
怎么做: 写成 data(){ return { num: 1 } }。这样每个 App 实例都有自己的 num。示例里的 console.log("data") 也能帮助观察:每创建一个实例,就会执行一次 data。
易错点: 不要写成 data: { num: 1 };也不要把外部对象直接 return obj,否则多个实例会共享同一份数据。data 函数必须返回对象,不能返回 undefined。

对照实验:App 与 BiaoGe 的差异 ​

为什么需要: 只看结论容易忘,必须用代码制造对比。示例里 App 每次 return { num:1 },BiaoGe 则 return obj,而 obj 是组件外部定义的一个对象。
怎么想: 如果 BiaoGe 的 6 个实例都返回同一个 obj,那么它们操作的就是同一个 num。点击任意一个 BiaoGe 按钮,this.num++ 改的是共享对象上的 num,所以其他 BiaoGe 也会一起变化。
怎么做: 运行页面后分别点击 App 按钮和 BiaoGe 按钮。会看到 App 的按钮各变各的;BiaoGe 的按钮则容易一起变化。调试时打开控制台,观察 data 的输出,也能理解每个组件实例创建时都会执行 data。
易错点: 不要误以为“写成函数就一定独立”。关键不只是函数,而是函数每次返回新对象。如果函数内部 return obj,仍然是共享。

从转写稿的工厂函数理解 data ​

为什么需要: 转写稿里用“工厂函数”解释过对象创建:字面量对象在多次声明相同结构时比较繁琐,工厂函数可以按需求返回对象。这个思路正好解释 data。
怎么想: data 不是普通配置值,而是“实例初始化时要执行的生产过程”。转写稿还提到,函数定义好之后作用域链条就确定了,调用时按定义位置找数据。放到组件里理解:每个组件实例都应该拥有自己的数据作用域,而不是共用外部变量。
怎么做: 把 data 写成函数,在函数体内 return 一个新对象;如果确实需要初始逻辑,可以在 return 前写,例如示例中的 console.log("data")。
易错点: 不要把“函数”理解成“随便包一层”。核心是返回新对象;也不要在多个组件之间复用同一个外部对象作为 data 的返回值。

易错点与小结 ​

  • data 写成对象:多个组件实例可能共享数据。
  • data(){ return obj }:虽然形式是函数,但返回的是同一个外部对象,仍然共享。
  • data 函数没有返回值:Vue 无法拿到组件初始数据。
  • 认为“组件写几次就只执行一次 data”:实际每个组件实例都会执行一次 data,示例里的 console.log("data") 就是观察点。
  • 本节结论:组件 data 写成函数,是为了让每个组件实例拿到独立的数据对象;函数每次返回新对象,数据才不会互相污染。

为什么需要父向子传属性 ​

父组件里的数据要交给子组件展示或使用,不能靠子组件自己“猜”。Vue 的做法是:父组件在使用子组件时,把数据以属性的形式写在子组件标签上;子组件通过 props 明确接收。为什么需要:让组件可复用,同一套子组件可以接收不同数据。怎么想:父组件负责“给”,子组件负责“声明接收”。怎么做:父组件写 :name="name",子组件写 props: ['name'] 或对象写法。易错点:父组件传了但子组件没接收,模板里写同名变量时,子组件会去 data 里找,不会自动使用父组件传来的值。课堂调试中,父组件传 a="1",子组件没写 props,模板 {{ a }} 显示的是子组件 data 里的 100;只有子组件声明接收后,才会使用传入值。

基本写法与同名冲突 ​

为什么需要:先跑通“父传子”,再谈类型限制。怎么想:子组件接收到的属性会作为实例属性使用,模板里可直接写。怎么做:

vue
<!-- 父组件 -->
<one :name="name" :age="18" />
js
// 子组件
export default {
  props: ['name', 'age']
}

易错点:props 里的名字不要和 data 里的名字重复。课堂里子组件同时写了 props: ['a'] 和 data(){ return { a: 100 } },Vue 会警告冲突;而且这个警告和父组件有没有传 a 没关系,只要 props 与 data 同名就会报。调试时先删掉冲突的 data.a,模板里的 a 才会稳定使用传入值。

类型限制:数组简写与对象写法 ​

为什么需要:只写 props: ['name'] 太松散,字符串、数字都能传,不利于发现错误。怎么想:需要限制类型时,把 props 写成对象。怎么做:简写可以写成数组,只限制类型;要设置必填、默认值,就写对象:

js
export default {
  props: {
    name: { type: String, required: true },
    age: { type: Number, default: 18 }
  }
}

对象写法还能一次限制多个属性。课堂调试:子组件要求 name 是 Number,父组件传字符串 "abc",控制台出现类型警告;改成数字 1 后警告消失。易错点:required: true 表示父组件必须传;default 表示父组件不传时使用默认值。二者不建议同时出现。父组件一旦传了值,default 就不生效。

对象、数组默认值必须用工厂函数 ​

为什么需要:对象和数组是引用类型,如果多个组件实例共用同一个默认对象,容易互相影响。怎么想:默认值需要“每次返回一个新对象/新数组”。怎么做:default 写成函数:

js
props: {
  info: {
    type: Object,
    default: () => ({ a: 1, b: 2 })
  },
  list: {
    type: Array,
    default: () => [1, 2, 3]
  }
}

课堂调试:info 默认值直接写对象会警告,改成 () => ({ a: 1, b: 2 }) 后正常;数组同理,必须用工厂函数返回数组。易错点:不要写成 default: { a: 1 } 或 default: [1,2,3];工厂函数要 return 对象或数组。

父向子传参综合示例 ​

为什么需要:列表渲染时,如果父组件写三遍结构,代码重复;封装成子组件后,父组件只负责传不同数据。怎么想:父组件把每本书的信息、标题、颜色传给子组件;子组件声明 props 后遍历展示。怎么做:

vue
<!-- 父组件 -->
<book-list
  v-for="book in books"
  :key="book.id"
  :book-info="book"
  :title="book.type"
  :color="book.color"
/>
vue
<!-- 子组件 book-list -->
<template>
  <div>
    <h3 :style="{ color }">{{ title }}</h3>
    <div v-for="item in bookInfo" :key="item.id">
      {{ item.name }} —— {{ item.author }}
    </div>
  </div>
</template>
<script>
export default {
  props: {
    bookInfo: { type: Array, required: true },
    title: { type: String, default: '' },
    color: { type: String, default: 'yellow' }
  }
}
</script>

易错点:父组件模板里 :book-info="book" 对应子组件 bookInfo,建议模板中写 kebab-case;v-for 要加 :key;required 属性没传会警告。

关联:子改父与 .sync ​

为什么需要:父组件把属性传给子组件后,子组件不能直接改 props。要改父组件数据,应通知父组件。怎么想:子组件通过 this.$emit('事件名', 参数) 触发自定义事件;父组件用 @事件名 接收。怎么做:

vue
<!-- 父组件 -->
<one :name.sync="name" />

等价于:

vue
<one :name="name" @update:name="val => name = val" />

子组件中:

js
this.$emit('update:name', '新值')

.sync 同时做两件事:把 name 作为属性传给子组件;监听 update:name 事件并更新父组件 name。易错点:事件名不要大写;自定义事件不需要在 props 里声明;子组件不要直接修改 props。课堂调试显示,用 .sync 后点击子组件按钮可以关闭/显示父组件内容,本质仍是子组件触发事件、父组件更新数据。

父传子:props 接收与类型限制 ​

为什么需要:父组件要把数据交给子组件渲染,但子组件不能直接改父组件数据。Vue 用 props 做“单向数据流”:父传子,子组件只读。怎么想:父组件在标签上写属性,子组件必须用 props 声明接收;没声明,模板里的同名变量会去 data 里找。怎么做:

html
<!-- 父组件 -->
<One :name="name" :age="80" />
js
// 子组件
export default {
  props: {
    name: {
      type: String,
      required: true
    },
    age: {
      type: Number,
      default: 18
    },
    info: {
      type: Object,
      default: () => ({ a: 1, b: 2 })
    },
    list: {
      type: Array,
      default: () => [1, 2, 3]
    }
  }
}

props 可写成数组(不限制类型),也可写成对象。对象里可写 type、required、default。如果类型是数组或对象,default 必须写成工厂函数返回默认值。

易错点:props 与 data 同名会警告,和父组件是否传值无关;props 只读,不能直接改;required 与 default 不建议同时出现;对象/数组默认值不要直接写 {} 或 []。

父传子示例:图书列表 ​

为什么需要:同类结构重复写多个 v-for 很繁琐,应抽成子组件。怎么想:父组件持有图书数组,按类型分组后把 book、title、color 传给子组件。怎么做:

html
<BookList :book="wuxia" title="武侠小说" color="blue" />
<BookList :book="yanqing" title="言情小说" color="pink" />
html
<div>
  <h3 :style="{ color: color }">{{ title }}</h3>
  <div v-for="item in book" :key="item.id">
    {{ item.name }} —— {{ item.author }}
  </div>
</div>

易错点:v-for 里 :key 的冒号容易漏;子组件没在 props 里接收 book/title/color,模板会报未定义;父组件传数组时不要写成字符串。

子传父:父传函数给子组件调用 ​

为什么需要:子组件要“关闭自己”或修改父组件状态,但子组件不能直接改父数据。怎么想:父组件定义函数,函数里更新父数据;把函数当属性传给子组件,子组件接收后调用。怎么做:

html
<!-- 父 -->
<One :isShow="isShow" :changeIsShow="changeIsShow" />
js
// 父
methods: {
  changeIsShow(val) {
    this.isShow = val
  }
}
js
// 子
props: ['isShow', 'changeIsShow'],
methods: {
  close() {
    this.changeIsShow(false)
  }
}

易错点:函数名在父标签上要用 : 绑定;子组件没接收函数就不能调用;函数里的 this 指向定义它的父组件实例,不是子组件。

子传父:自定义事件 $emit ​

为什么需要:传函数可行,但父组件要额外写属性;自定义事件更符合“子组件通知父组件”的语义。怎么想:父组件用 @事件名="处理函数" 监听;子组件用 this.$emit('事件名', 参数) 触发。怎么做:

html
<!-- 父 -->
<Two :name="name" @随便="changeName" />
js
// 父
methods: {
  changeName(n) {
    this.name += n
  }
}
js
// 子
methods: {
  add() {
    this.$emit('随便', 2)
  }
}

易错点:自定义事件不需要在 props 里声明;事件名建议全小写;$emit 第一个参数是事件名,后面才是参数;父组件若写成 @随便="changeName(3)",子组件 $emit 再传的参数就传不进去。

sync 修饰符:传属性 + 更新事件 ​

为什么需要:父传属性给子、子又要改这个属性的场景很多。怎么想:.sync 是语法糖,同时做两件事:向子组件传属性,并监听 update:属性名 事件。怎么做:

html
<!-- 父 -->
<App :isShow.sync="isShow" />
js
// 子
props: ['isShow'],
methods: {
  close() {
    this.$emit('update:isShow', false)
  }
}

它等价于传 isShow 属性,并监听 update:isShow,事件处理函数把新值赋回父组件的 isShow。

易错点:子组件触发的事件名必须是 update:属性名;父组件绑定属性时不要漏 .sync;不要同时在子组件里直接改 props。调试时先看父组件值是否变化,再看子组件 $emit 的事件名是否完全一致。

本节先解决什么:从 props 类型限制到父子通信 ​

本节标题是“非父子传递参数”,但转写稿实际先把父子组件通信链路补齐:父传子、子向父、自定义事件、.sync。转写稿中明确提到“非父子”留到后续,因此这里不编造未讲方案,先把本节出现的 props 类型限制和父子传参讲透。

props 声明与 data 同名冲突 ​

为什么需要:父组件给子组件传值,子组件必须显式接收;否则模板里使用的变量会去 data 中找。
怎么想:先声明 props,再在模板中使用;props 与 data 不能同名。
怎么做:

js
Vue.component('One', {
  template: '<div>{{ a }}</div>',
  props: ['a'],
  data() {
    return { a: 100 } // 错误:a 已由 props 声明
  }
})

调试时会看到警告:data property "a" already defined as a prop。把 data 中的 a 去掉,模板里的 a 才来自父组件传入值。
易错点:这个冲突警告与父组件有没有传 a 无关,只与子组件同时声明 props.a 和 data.a 有关。

对象写法:type、required、default ​

为什么需要:数组写法太松散,字符串、数字都能传,约束不明确。
怎么想:用对象描述每个 prop 的类型、是否必填、默认值。
怎么做:

js
props: {
  name: { type: Number, required: true },
  userName: { type: String, default: '张三' },
  age: { type: Number, required: true }
}

若 name 传字符串 "abc",会提示类型不符;若 userName 不传,可使用默认值;若 age 标了 required: true 却不传,会警告。
易错点:当默认值是对象或数组时,default 必须是工厂函数:

js
props: {
  info: {
    type: Object,
    default: () => ({ a: 1, b: 2 })
  }
}

直接写 default: { a: 1 } 会报错,因为对象/数组默认值要求通过工厂函数返回。

父传子渲染列表:把数据交给子组件 ​

为什么需要:同一结构渲染多次时,应抽成组件,避免重复写多个 v-for。
怎么做:

js
// 父
<BookList
  :book="wuxiaList"
  title="武侠小说"
  color="blue"
/>

// 子
props: {
  book: { type: Array, required: true },
  title: { type: String, default: '图书' },
  color: { type: String, default: 'yellow' }
},
template: `
  <div>
    <h3 :style="{ color: color }">{{ title }}</h3>
    <div v-for="item in book" :key="item.id">
      {{ item.name }} —— {{ item.author }}
    </div>
  </div>
`

易错点:v-for 要写 :key;父组件传多个属性时注意冒号,动态值用 :,普通字符串可不用。

子向父:把函数当属性传下去 ​

为什么需要:子组件不能直接改父组件状态,否则显示隐藏等逻辑会冲突。转写稿中用子组件自己维护 bool 控制显示隐藏,结果与父级 isShow 互相干扰。
怎么想:父级定义“修改状态”的函数,把函数传给子组件;子组件调用函数,让父级自己更新。
怎么做:

js
// 父
<Child :changeIsShow="changeIsShow" />
methods: {
  changeIsShow(value) {
    this.isShow = value
  }
}

// 子
props: { changeIsShow: Function },
template: '<button @click="changeIsShow(false)">关闭</button>'

易错点:函数名、属性名要一致;子组件只负责调用,不直接写 this.$parent.isShow = false。

自定义事件:$emit 触发父级处理 ​

为什么需要:比“传函数”更符合组件事件模型。
怎么做:

js
// 父
<Child :name="name" @random="changeName" />

// 子
methods: {
  trigger() {
    this.$emit('random', 2)
  }
}

父级 @random="changeName" 表示监听自定义事件 random;子组件 this.$emit('random', 2) 触发事件并传参。
易错点:自定义事件名不要大写开头;$emit 第一个参数是事件名,后面才是参数。事件名要与父级 @ 后名称一致。

.sync 修饰符:属性与 update 事件合并 ​

为什么需要:父传子属性、子又要通知父更新,这种场景很多。
怎么想:.sync 同时做两件事:传属性、监听 update:属性名。
怎么做:

js
// 父
<Child :isShow.sync="isShow" />

// 子
this.$emit('update:isShow', false)

等价于父级监听 @update:isShow="val => isShow = val"。转写稿中把显示隐藏改为 .sync 后,子组件触发 update:isShow 即可关闭自身。
易错点:事件名必须是 update: 加属性名;.sync 不是“自动双向绑定”,本质仍是子组件发事件、父组件更新。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例 1:为什么组件的 data 必须是函数(6-7) ​

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>data 为什么是函数</title>
  <script src="https://unpkg.com/vue@2/dist/vue.js"></script>
</head>
<body>
<div id="app">
  <app></app>
  <app></app>
  <hr/>
  <biao-ge></biao-ge>
  <biao-ge></biao-ge>
</div>
</body>
<script>
  // 组件外部的一个"公共对象"
  let obj = { num: 1 }

  new Vue({
    el: "#app",
    components: {
      // 正确写法:data 是函数,每次返回一个全新的对象
      App: {
        data() {
          console.log("data");
          return { num: 1 }
        },
        template: `<button @click="changeNum">App:{{num}}</button>`,
        methods: {
          changeNum() { this.num++ }
        }
      },
      // 错误示范:data 返回同一个对象的引用
      BiaoGe: {
        data() {
          return obj
        },
        template: `<button @click="changeNum">BiaoGe:{{num}}</button>`,
        methods: {
          changeNum() { this.num++ }
        }
      }
    }
  })
</script>
</html>

关键行与易错点

  • data() { return { num: 1 } }:组件里 data 必须写成函数并返回对象,不能写成对象字面量(写成对象会直接报错:data 必须是一个函数)。
  • 每个 <app> 实例都会调用一次 data,各自拿到一个新对象,所以点 App 的按钮只影响自己。
  • BiaoGe 里 return obj 返回的是同一个外部对象,多个实例共享一份数据:点其中一个,所有 BiaoGe 的 num 一起变——这正是要把 data 设成函数的原因(单例对象 → 数据被污染)。
  • 结论:函数返回对象可以让组件中的数据独立;而且写成函数后,还能在 return 前写逻辑(加工初始数据)。

示例 2:父向子传参 + props 类型限制(6-8) ​

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>父向子传参</title>
  <script src="https://unpkg.com/vue@2/dist/vue.js"></script>
</head>
<body>
<div id="app">
  <!-- 冒号不能漏,漏了就是把字符串 "wuxia" 传过去 -->
  <book-list :book="wuxia" title="武侠小说" color="blue"></book-list>
  <book-list :book="yanqing" title="言情小说" color="pink"></book-list>
  <book-list :book="xuanhuan" title="玄幻小说" color="green"></book-list>
</div>
</body>
<script>
  new Vue({
    el: "#app",
    data: {
      wuxia: [
        { id: 1, bookName: "鹿鼎记", author: "金庸" },
        { id: 2, bookName: "圆月弯刀", author: "古龙" }
      ],
      yanqing: [
        { id: 3, bookName: "情深深雨濛濛", author: "琼瑶" },
        { id: 4, bookName: "明朝王爷爱上我", author: "艺鸣" }
      ],
      xuanhuan: [
        { id: 5, bookName: "斗罗大陆", author: "唐家三少" },
        { id: 6, bookName: "吞噬星空", author: "我吃西红柿" }
      ]
    },
    components: {
      BookList: {
        // 数组写法只能限制类型;对象写法才能限制类型 + 必填 + 默认值
        props: {
          book: { type: Array, required: true },
          title: { type: String, default: "标题" },
          color: { type: String, default: "yellow" }
        },
        template: `
          <div>
            <h3 :style="{ color: color }">{{ title }}</h3>
            <div v-for="item in book" :key="item.id">
              {{ item.bookName }} —— {{ item.author }}
            </div>
            <hr/>
          </div>
        `
      }
    }
  })
</script>
</html>

关键行与易错点

  • 父组件通过属性把数据传给子组件::book="wuxia" 是传数组,写成 book="wuxia" 传的是字符串,子组件类型校验会警告。
  • props 推荐写成对象形式,才能用 type(类型)、required(是否允许省略)、default(默认值);写成数组或直接写类型只能限制类型。
  • required: true 表示属性不能省略;default 只在没传时生效,传了就用传的值。
  • 如果默认值是对象或数组,default 必须写成工厂函数 default: () => ({...}),否则报错。
  • 子组件里的 props 是只读的,直接 this.book = xxx 会警告;props 名不能和 data 中的属性重名,否则也会警告。
  • v-for 必须加 :key,:style="{ color: color }" 中的 color 就是接收到的 props。

示例 3:子向父传参——函数 / 自定义事件 / .sync(6-9、6-10) ​

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>子向父传参</title>
  <script src="https://unpkg.com/vue@2/dist/vue.js"></script>
</head>
<body>
<div id="app">
  <button @click="isShow = !isShow">父组件:显示/隐藏</button>
  <hr/>
  <!-- 方式一:把父的函数以属性形式传下去 -->
  <one :name="name" :change-name="changeName"></one>
  <!-- 方式二:自定义事件(@事件名) -->
  <two :name="name" @suibian="changeName"></two>
  <!-- 方式三:.sync 修饰符 -->
  <three :is-show.sync="isShow"></three>
</div>
</body>
<script>
  new Vue({
    el: "#app",
    data: {
      name: 1,
      isShow: true
    },
    methods: {
      // 父组件负责更新自己的数据状态
      changeName(n) {
        this.name += n
      }
    },
    components: {
      one: {
        props: {
          name: Number,
          changeName: Function
        },
        template: `<button @click="changeName(1)">one +1(当前{{name}})</button>`
      },
      two: {
        // 自定义事件不需要在 props 里声明
        props: {
          name: Number
        },
        template: `<button @click="$emit('suibian', 2)">two +2(当前{{name}})</button>`
      },
      three: {
        props: {
          isShow: Boolean
        },
        template: `
          <div v-show="isShow">
            three
            <button @click="$emit('update:isShow', false)">关闭自身</button>
          </div>
        `
      }
    }
  })
</script>
</html>

关键行与易错点

  • 方式一:父组件在 methods 里定义函数(负责改数据),用 :change-name="changeName" 传给子组件;子组件用 props: { changeName: Function } 接收后调用。子组件不直接改父数据,而是调用父给的函数,所以数据仍然是父自己的。
  • 方式二:父组件在使用组件时写 @suibian="changeName",这就是自定义事件,suibian 是自定义的事件名(全小写);子组件通过 this.$emit('suibian', 参数)(模板里写 $emit)触发。自定义事件不需要、也不应该写在 props 里。
  • $emit 的第一个参数是事件名,后面的参数会传给父的事件处理函数;如果父在 @suibian="changeName(3)" 里已经写了参数,子组件再传参就收不到了。
  • 方式三 .sync::is-show.sync="isShow" 同时干了两件事——①把 isShow 作为属性传给子组件;②自动监听 update:isShow 事件,并把它赋值回 isShow。所以子组件只要 $emit('update:isShow', false) 就能改父数据。
  • 注意 .sync 触发的事件名必须是 update:属性名(这里属性是 isShow,事件就是 update:isShow),写错事件名父数据不会变。
  • 易错点:子组件自己加 v-show 内部状态去隐藏,会和父组件的显示/隐藏互相冲突;要隐藏自身,应当通知父组件去改状态(如示例中的 .sync 写法)。

常见错误 ​

1. 组件 data 写成对象字面量

  • 现象:页面里放了 6 个 <biao-ge>,点其中一个按钮,6 个数字一起变。
  • 原因:data(){ return obj } 让所有实例共用同一个对象引用,组件间数据没有隔离。
  • 修法:data(){ return { num: 1 } },每次实例化返回全新对象;确实要复用初始结构时写成 return { ...obj } 或工厂函数。

2. props 与 data 同名

  • 现象:控制台报 The data property "a" is already declared as a prop,且模板里 {{a}} 取到的值说不清是父传的还是自己 data 的。
  • 原因:props 里的键和 data() 返回对象里的键重名,与父组件有没有真的传值无关(去掉传参警告依旧存在)。
  • 修法:二者改名区分,例如 prop 叫 initNum、data 叫 num;内部状态用 data 承接 prop 的初值。

3. props 只用数组写法却想加约束

  • 现象:父组件传 name="abc" 和 :age="1" 都不报错,写错类型也悄悄通过;想设默认值无处可写。
  • 原因:props: ['name'] 只声明名字,不做类型校验,也不支持 required / default。
  • 修法:改成对象写法 props: { name: { type: String, required: true, default: '张三' } };注意 type 要写构造函数 Number,写成字符串 'Number' 校验会失效。

4. default 给对象/数组写成字面量

  • 现象:info: { type: Object, default: { a: 1, b: 2 } } 报 Props with type Object/Array must use a factory function。
  • 原因:对象字面量会被所有实例共享,Vue 要求用工厂函数每次返回新对象。
  • 修法:default: () => ({ a: 1, b: 2 }),数组同理 default: () => [1, 2, 3]。

5. required 与 default 同时写

  • 现象:以为不传值时能落到默认值,结果依旧报 Missing required prop,或默认值根本不生效。
  • 原因:required: true 的含义是"父组件必须传",与默认值语义冲突;只要父组件没传就报错,默认值轮不到生效。
  • 修法:二者二选一——必填就删 default,可省略就删 required。

6. 子组件直接改 props

  • 现象:子组件里 this.name++,控制台警告 Avoid mutating a prop directly,父组件数据不变,父子显示不同步。
  • 原因:props 是单向数据流,子组件只读;父组件重新渲染时会用旧值覆盖子组件的修改。
  • 修法:走两条路——父组件把函数当属性传下来(this.$emit 之前的老写法),或用自定义事件 this.$emit('update:name', newVal) 让父组件自己改。

7. 自定义事件名用驼峰或带大写

  • 现象:父组件写了 @updateName="fn",子组件 $emit('updateName'),函数就是不执行。
  • 原因:HTML 模板中的属性名不区分大小写,updateName 被解析成 updatename,与 emit 的名字对不上。
  • 修法:事件名统一用 kebab-case,如 @update-name + $emit('update-name');.sync 场景必须严格写 update:propName。

8. 监听时加了括号导致参数丢失

  • 现象:@my-event="changeName(3)" 里写死了 3,子组件 $emit('my-event', 100, 200) 传来的值收不到。
  • 原因:内联语句把事件处理函数变成了固定调用,$emit 的参数不会自动注入。
  • 修法:要么写成方法名 @my-event="changeName",要么用 $event 显式接收 @my-event="changeName($event)"(多参用 $emit('e', {a, b}) 打包成一个对象)。

9. 模板里用了没声明的名字

  • 现象:模板里写 {{ a }} 报 Property or method "a" is not defined;给组件传了 :a="80" 仍然报同样的错。
  • 原因:模板取值的查找范围只有 data / props / methods / computed,传了属性但没在 props 里声明,等于没接收。
  • 修法:先 props: { a: Number } 声明再使用;不打算接收的属性干脆别传。

练习 ​

练习 1|组件复用隔离 写一个 <counter> 组件,内部有 count 和"+1"按钮,在页面上渲染 5 个。要求:点任意一个按钮只影响自己。然后故意把 data 改成 data: { count: 0 },观察并解释 5 个按钮联动的原因,再改回来。

练习 2|props 约束全套 写一个 <book-item> 组件,接收:title(字符串,必填)、author(字符串,默认"佚名")、price(数字,必填)、tags(数组,默认 ['未分类'])、info(对象,默认 { stock: 0 })。父组件传 3 本书:一本完整、一本缺 author 和 tags、一本故意把 price 传成字符串 "39"。要求分别观察控制台警告,并说明每条警告由哪条规则触发。

练习 3|父传子 + 列表渲染 用 books 数组(武侠/言情/玄幻三类,每项含 id、name、author)渲染三个 <book-list>,每块带不同标题和不同文字颜色。要求颜色通过 prop 传入并设默认值 yellow,同时父组件不传颜色时也能正常显示。

练习 4|子传父两种写法对比 实现"父组件显示数字,子组件里放按钮点一次加 1",分别用:(a) 父组件把 changeNum 函数当属性传给子组件;(b) 自定义事件 $emit。写完后回答:为什么子组件里不能直接 this.num++?

练习 5|.sync 改造 把上一题的显示/隐藏切换(父组件控制子组件显隐,子组件内部有"关闭自己"按钮)改写成 .sync 版本。要求:父组件只写 :is-show.sync="isShow",子组件通过 $emit 更新。再故意把事件名写成 update-isShow 和 isShow,记录两种写法的结果差异。

原视频对照 ​

#视频时长
1类型限制25:39
2传递参数示例12:15
3子向父通过函数传递参数16:34
4自定义事件22:02
5sync修饰符17:31

小结 ​

  1. 组件 data 必须是函数,本质是让每个实例拿到独立的对象引用;写成对象就等于全页面共享一份状态,这是"改一个全变"的唯一根因。
  2. props 有两种写法:数组只声明名字,对象才能做类型校验、必填、默认值;一旦需要约束就必须换对象写法。
  3. props 校验的三条硬规则:type 写构造函数不写字符串;required 与 default 互斥;对象/数组的 default 必须用工厂函数返回。
  4. 同名冲突只发生在 props 与 data/methods 之间,与父组件传没传值无关,遇到 already declared 警告先改名字。
  5. props 是只读的单向数据流,子组件改 props 只会得到警告和父子数据不一致;想改必须让父组件来改。
  6. 子传父只有两条路:把父函数当属性传下去由子调用,或用 $emit 触发自定义事件;.sync 是第二条路的语法糖,它同时做了"传属性"和"监听 update:xxx"两件事。
  7. 事件名一律用小写 kebab-case,.sync 的 emit 名必须严格写成 update:propName,否则监听不到且不报错,排查成本极高。
  8. 传参丢失多半出在监听侧:@e="fn(3)" 会吃掉 $emit 的参数,要么只写方法名,要么用 $event 显式接住。