Skip to content

组件嵌套与组件示例(Lesson4 2/3) ​

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

学习目标 ​

  • 能使用 Vue 组件完成页面嵌套:将示例拆分为根组件、评论列表组件和分页组件,并正确注册、嵌套与渲染。
  • 能说明 Vue 实例与组件实例的关系,指出组件实例中 this 的指向、原型链关系及 Vue.prototype 自定义属性的访问路径。
  • 能编写组件 data 为返回对象的函数,并解释其必要性,能通过多实例示例验证数据互不影响及共享引用对象导致的问题。
  • 能通过 props 实现父向子传递参数,正确区分静态属性与 :属性 动态绑定,并处理驼峰与短横线命名。
  • 能识别并修正子组件直接使用父级数据、修改 props、props 与 data/方法/过滤器重名等错误,并说明报错原因。
  • 能将内联模板抽离为独立组件文件,通过模块化方式导入注册,并验证抽离前后渲染结果一致。

知识点 ​

先看目标:把 App 和 Child 嵌起来 ​

组件嵌套解决的是“界面有层级,代码也要有层级”的问题。外层页面可以拆成 App 组件,App 里面再放 Child 组件。这样每层只关心自己的模板和注册范围,阅读和调试都更清楚。

思路是:谁注册,谁使用。根 Vue 实例的 components 里注册 App,所以 App 只能在根实例模板里用;App 自己的 components 里注册 Child,所以 Child 只能在 App 模板里用。做法就是保留下面这段笔记代码:

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>
</div>
</body>
<script>
	new Vue({
		el: "#app",
		// 在该components属性中声明的组件只允许在Vue实例中使用
		components: {
			// 声明了一个名字为App的组件
			App: {
				// 指定组件渲染内容
				template: `
					<div>
						<h3>App组件</h3>
						<Child></Child>
					</div>
				`,
				//  在该components属性中声明的组件只允许在组件App中使用
				components: {
					Child: {
						template: `
							<div>
								<h3>Child</h3>
							</div>
						`
					}
				}
			}
			
		}
	})
</script>
</html>

易错点:components 不要拼错;template 要用反引号包住多行模板;Child 必须注册在 App 的 components 里,不能只写在根实例里。调试时如果页面只出现 App、不出现 Child,先检查 Child 是否在 App 的注册范围内。

抽离组件:从“能跑”到“好读” ​

为什么需要抽离?当 template 里又套 template,代码一层又一层,功能能跑但阅读困难。思路是把一个组件写成一个独立 JS 文件,导出组件配置对象,父组件通过 import 引入后再注册。做法是:在 components 目录下建 Child.js,写 export default { template: &#96;...&#96; },在 App 所在文件里 import Child from './components/Child.js',然后 components: { Child }。

易错点:export default 对应 import Child from ...;路径和 .js 后缀要写对;抽离后仍然要在父组件里注册,不能只 import 不注册。转写稿里还提醒:写多行模板时先写括号再写反引号,养成整体阅读的习惯。

组件实例和 Vue 实例是什么关系 ​

为什么需要理解?因为组件里的 this 很容易被误认为根 Vue 实例。实际上,new Vue() 得到的是 Vue 实例;组件里的 this 是 VueComponent 实例。调试时在组件方法里打印 this,会看到它来自 VueComponent。

怎么想:组件实例的原型链上能找到 Vue 的原型。转写稿的推导是:VueComponent.prototype.__proto__ === Vue.prototype。所以给 Vue.prototype.$userName = '张三' 后,根实例和组件里都能用 this.$userName。怎么做:全局属性建议以 $ 开头,例如 Vue.prototype.$userName。易错点:组件不能直接使用根实例的 data、methods;全局属性命名要避免和组件自身状态重名。

组件中的 data 必须是函数 ​

为什么需要?同一个组件可能被使用多次,每次都应该有独立数据。若写成对象,多个组件实例会共享同一份数据。转写稿里用引用类型解释:data(){ return obj } 返回同一个对象,改一个组件会影响另一个;data(){ return { name: 1 } } 每次都返回新对象,互不影响。

怎么做:组件里写 data() { return { name: 1 } }。易错点:data: { name: 1 } 在组件中会报错;函数必须返回对象;不要返回外部同一个对象。调试时可点击两个相同组件里的按钮,若数据互相影响,就检查是否返回了同一个引用。

父向子传参:靠属性,不靠“直接拿” ​

为什么需要?子组件不能直接使用父组件的数据。思路是父组件通过属性把数据传给子组件,子组件用 props 声明接收。做法:父模板写 <Child :user-name="userName"></Child>,子组件写 props: ['userName']。加冒号表示绑定表达式,不加冒号通常传字符串。

易错点:DOM 模板里属性名建议用横线,如 user-name,子组件 props 里写驼峰 userName;props 是只读的,不要在子组件里直接 this.userName += '!',否则会警告且父数据不会按预期改变;props 名不要和 data、methods、过滤器重名,否则视图里分不清用的是哪一个。

组件示例与嵌套:先把静态页面挂到 Vue 上 ​

为什么需要:组件示例能把“根实例—子组件—孙组件”的关系一次看清。笔记里的微博界面不复杂,但包含输入区、留言列表和分页,很适合练嵌套。

怎么想:先不要急着拆文件,先把静态结构拿过来,用 Vue 挂载最外层 .jyArea,再把输入区、留言区、分页分别声明成组件。

怎么做:保留下面示例代码。TakeComment 用 #takeTp 模板,CommentOn 用 #commentTp 模板,并在 CommentOn 的 components 里再注册 PageList,模板中用 <page-list></page-list>。

html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Title</title>
	<link href="style/weibo.css" rel="stylesheet" type="text/css" />
	<script src="lib/vue.js"></script>
</head>
<body>
<div class="jyArea">
	<take-comment></take-comment>
	<comment-on></comment-on>
</div>
<template id="takeTp">
	<div class="takeComment">
		<textarea name="textarea" class="takeTextField" id="tijiaoText"></textarea>
		<div class="takeSbmComment">
			<input type="button" class="inputs" value="" />
		</div>
	</div>
</template>
<template id="commentTp">
	<div class="commentOn">
		<div class="noContent">暂无留言</div>
		<div class="messList">
			<div class="reply">
				<p class="replyContent">内容</p>
				<p class="operation">
					<span class="replyTime">2020.03.10 01:01:01</span>
					<span class="handle">
                    	<a href="javascript:;" class="top">0</a>
                        <a href="javascript:;" class="down_icon">9</a>
                        <a href="javascript:;" class="cut">删除</a>
                    </span>
				</p>
			</div>
		</div>
		<page-list></page-list>
	</div>
</template>
</body>
<script>
	new Vue({
		el:".jyArea",
		components:{
			TakeComment:{
				template:"#takeTp"
			},
			CommentOn:{
				template:"#commentTp",
				components:{
					PageList:{
						template:`
							<div class="page">
							    <a href="javascript:;" class="active">1</a>
							    <a href="javascript:;">2</a>
							    <a href="javascript:;">3</a>
							</div>
						`
					}
				}
			}
		}
		
	})
</script>
</html>

易错点:组件名在模板里通常写成短横线形式,如 take-comment;PageList 必须注册在 CommentOn 内部,否则它不能在 CommentOn 的模板里使用。转写稿里反复调试“两个组件是否显示、分页是否嵌进去”,本质就是检查注册位置和模板位置。

抽离组件与模块化:让模板和 JS 文件分开 ​

为什么需要:都写在 HTML 里会出现多个 <template>,一层套一层,阅读困难。转写稿建议把 TakeComment、CommentOn、PageList 抽到 components 目录下,各自一个 .js 文件。

怎么想:每个组件导出的是一个配置对象,里面有 template;需要子组件时,在父组件文件里 import,再注册到 components。

怎么做:例如 takeComment.js 里 export default { template: ... },主文件用 <script type="module">,再 import TakeComment from "./components/takeComment.js"。commentOn.js 里引入 PageList 并注册;pageList.js 单独导出分页模板。转写稿还演示了模块化:export const A = 1; export const B = 2; 对应 import { A, B } from ...;export default 对应默认导入。调试时通过控制台和 Ctrl 点击 import 路径确认组件来源。

易错点:template 建议用反引号包整体,先写括号再换行,阅读性更好;import 路径和 .js 后缀要写对;script 要加 type="module"。转写稿强调“模块化是重点”,不能看到 import/export 还靠猜。

组件实例与 Vue 实例的关系:this 不是根实例 ​

为什么需要:组件里的 this 指向 VueComponent 实例,不是 new Vue 的根实例。理解原型链,才能明白为什么根实例原型上加的东西,组件里也能找到。

怎么想:new Vue() 得到根实例;组件实例由 VueComponent 构造。转写稿的结论是:组件实例的隐式原型链上能关联到 Vue 的原型,所以在 Vue.prototype 上挂 $userName 这类自定义属性,根实例和组件里都能访问。

怎么做:可在 Vue.prototype.$userName = "..." 后,在组件里 this.$userName 访问。转写稿用 this.__proto__.__proto__ 观察原型链,说明当前实例找不到就沿原型链找。

易错点:不要以为组件里的 this 就是根 vm;自定义属性一般建议以 $ 开头,这是建议不是语法。原型链概念不清楚时先记住结论,后面项目里会反复用到。

组件中的 data:必须是函数并返回对象 ​

为什么需要:组件可能被使用多次,如果 data 直接写对象,多个组件实例会共享同一引用,改一个全变。转写稿用“韦小宝和七个老婆”的比喻说明引用地址共享。

怎么想:组件 data 写成函数,每次实例化都返回一个新对象,相当于工厂函数,保证对象独一无二;返回前还可以写逻辑。

怎么做:

js
data() {
  return {
    name: 1
  }
}

易错点:写成 data: { name: 1 } 会报错;写成函数但不 return 对象也会报错。转写稿调试时先看到“data 应该是函数”,再看到“函数应返回对象”,最后才正常。若返回外部同一个对象,多个组件会联动,这是错误示范。

父向子传参:用 props 接收,且属性只读 ​

为什么需要:子组件不能直接使用父级 data。转写稿里在子组件模板直接写父级的 username 会报“未定义”,所以要通过属性向下传。

怎么想:父模板里写 <app user-name="张三"></app> 传的是字符串;写 :user-name="username" 才是把父级数据传下去。子组件必须用 props 声明允许接收的属性。

怎么做:

js
props: ["userName"]

DOM 模板中驼峰属性常写成短横线:父级写 user-name,子级 props 接收 userName。转写稿还试了 password、pass-word,说明带横线的写法接收时要对应驼峰。

易错点:props 是只读的,子组件里直接 this.userName += "!" 会报警告,而且不会改父级数据;props 中声明过的名字不要和 data、methods、filters 重名,否则视图里分不清用的是哪个。转写稿的调试过程就是:先不加冒号传字符串,再加冒号传数据,最后观察只读警告和命名冲突警告。

先看现象:两个按钮分别在谁里面执行 ​

本节要弄清:new Vue() 得到的实例,和组件配置对象生成的组件实例,到底是不是一回事;它们之间又通过什么发生联系。示例里有两个按钮:根实例模板中的“点我”调用 fn,组件 one 模板中的“one”调用 fn2。为什么需要先分清?因为以后写组件时,this 指向谁、能不能访问 Vue.prototype 上的公共属性,都会直接影响代码能不能跑。怎么想?先记住结论:根实例由 Vue 构造出来;组件实例由组件构造函数 VueComponent 构造出来,不是同一个对象。怎么做?运行下面代码,分别点击两个按钮看控制台。易错点:不要因为都能访问 $useName,就误以为组件实例就是根实例。

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">
	<button @click="fn">点我</button>
	<hr/>
	<one></one>
</div>
</body>
<script>
	Vue.prototype.$useName = "zhangsan";
	const vm = new Vue({
		el:"#app",
		methods:{
			fn(){
				console.log(this === vm);// true
				console.log(this.__proto__ === Vue.prototype);// true
				console.log(vm.__proto__ === Vue.prototype);// true
				console.log(this.$useName)
			}
		},
		components:{
			// Vue配置对象中有什么属性,那么组件配置对象中就可以设置什么属性
			one:{
				template:(`
					<button @click="fn2">one</button>
				`),
				methods:{
					fn2(){
						// this是通过VueComponent实例化出来的
						// 组件实例与Vue实例有什么关联:this.__proto__.__proto__ === Vue.prototype
						// console.log("fn2",this);
						console.log(this.__proto__);// Vue实例
						console.log(this.__proto__.__proto__ === Vue.prototype);
						console.log(this.$useName)
					}
				}
			}
		}
	})
</script>
</html>

为什么需要原型链:公共属性只挂一次,实例都能查 ​

为什么需要 Vue.prototype.$useName?如果每个实例都手动加 $useName,根实例、组件实例都要重复写,维护成本高。转写稿用构造函数举例:给 Box.prototype 加属性,new Box() 的实例自己身上没有,就会沿着原型链找到。怎么想?访问属性时先找自身,找不到找 __proto__,再找不到继续往上找。怎么做?把 $useName 挂到 Vue.prototype,根实例沿 vm.__proto__ 找到;组件实例沿 this.__proto__.__proto__ 找到。易错点:转写稿里的“隐示原形/显示原形”应纠正为“隐式原型/显式原型”;__proto__ 是隐式原型,prototype 是构造函数显式原型。

怎么做:用三行 console 验证关系 ​

点击“点我”时,fn 里的 this 就是 vm,所以 this === vm 为 true;vm.__proto__ === Vue.prototype 为 true,因此 this.$useName 输出 zhangsan。点击“one”时,fn2 里的 this 是组件实例,由 VueComponent 实例化出来;它自身没有 $useName,于是先到 this.__proto__,再到 this.__proto__.__proto__,这里等于 Vue.prototype,所以输出 true 和 zhangsan。注意原代码中 console.log(this.__proto__);// Vue实例 这句注释不严谨:this.__proto__ 是组件构造函数的原型对象,不是 new Vue() 出来的那个根实例;真正和根实例发生联系的是它再往上一层等于 Vue.prototype。调试时建议先打印 this,再打印 this.__proto__,最后打印 this.__proto__.__proto__ === Vue.prototype,按顺序看原型链。

易错点:组件里的 this 不是根实例 ​

第一,组件 methods 里的 this 是组件实例,不是 vm;组件不能直接调用根实例的 fn,需要靠 props、自定义事件等方式通信。第二,Vue.prototype 适合挂公共方法或属性,自定义名字建议以 $ 开头,但这是建议不是语法。第三,组件配置对象虽然能写 methods、components 等,但 data 有特殊要求:根实例的 data 可以是对象,组件里的 data 必须是函数并返回对象,否则多个组件会共享同一份数据。第四,不要修改 props,也不要把 props 名和 data、methods、过滤器重名,否则视图里分不清用的是哪一个。把本节的原型链关系记住:根实例找 Vue.prototype 只隔一层;组件实例找 Vue.prototype 隔两层,即 this.__proto__.__proto__ === Vue.prototype。

组件里的 data 为什么不能照抄实例写法 ​

为什么需要:Vue 实例里写 data:{ userName:"vue实例" } 很顺手,但组件是会被复用的。如果组件里的 data 也直接写成对象,多个组件实例就可能共用同一份数据,改一个会影响另一个。

怎么想:转写稿里先按实例写法在组件中写 data,控制台报错,提示组件里的 data 应该是一个函数;改成函数但没有返回值,又提示必须返回一个对象。于是推导出:组件 data 的类型是函数,返回值必须是对象,对象里的属性才是组件自己的数据状态。

怎么做:在组件配置中写:

js
data(){
  return {
    userName:"one数据状态"
  }
}

易错点:不要写成 data:{...};也不要只写 data(){ } 而不 return。组件模板里只能直接使用本组件 data 返回的属性,不能直接拿父级实例的 userName 来用。

为什么写成工厂函数 ​

为什么需要:组件可能被使用多次。每次使用组件,都应该得到一份独立的数据状态。转写稿里用“函数每次返回一个新对象”来说明:第一次调用返回一个对象,第二次调用又返回另一个对象,引用地址不同,所以互不影响。

怎么想:把 data 想成“工厂函数”——每次创建组件实例时,调用一次函数,生产一份新对象。这样返回的对象是独一无二的;并且可以在 return 之前写一些逻辑。

怎么做:保留笔记中的写法:

js
data(){
  return {
    userName:"one数据状态"
  }
}

易错点:如果在函数外先定义一个对象,再在 data 里 return 同一个对象,多个组件实例仍会共享这份数据。转写稿现场演示过:一个组件按钮加一,另一个也跟着变,就是因为返回的是同一个引用。正确做法是每次 return 一个新对象。

完整示例代码 ​

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">
	<h3>{{userName}}</h3>
	<hr/>
	<One></One>
</div>
</body>
<script>
	//注意:在组件中声明数据状态data属性,类型是一个函数,返回值必须是一个对象,对象当中的属性即是组件中的数据状态。
	new Vue({
		el:"#app",
		data:{
			userName:"vue实例"
		},
		methods:{},
		filters:{},
		computed:{},
		
		components:{
			One:{
				// 写成工厂函数模式的好处:1- 返回的对象是独一无二的,2-可以在返回之前写一些逻辑
				data(){
					return {
						userName:"one数据状态"
					}
				},
				methods:{},
				filters:{},
				computed:{},
				template:(`
					<h3>one:{{userName}}</h3>
				`)
			}
		}
	})
</script>
</html>

怎么做:实例里的 userName 显示“vue实例”,组件 One 里的 userName 显示“one数据状态”,两者互不干扰。转写稿还提醒:组件中的 methods、filters、computed 等也只能使用组件自身声明的,不能直接使用外部的。

易错点:组件模板中写 {{userName}} 时,找的是组件自身 data 返回的对象属性;找不到就报未定义。不要误以为子组件能直接读父级实例的数据。

调试过程与易错点归纳 ​

为什么需要:知道规则还不够,要能看懂报错。转写稿里的调试顺序是:先写对象形式 → 报错;再写函数不返回对象 → 报错;最后写函数返回对象 → 正常渲染。

怎么想:看到组件 data 报错,先检查两点:是不是函数?函数有没有 return 一个对象?再检查模板里用的属性是否在返回对象中声明。

怎么做:按“函数 + return 对象”固定模板写;需要初始逻辑时,在 return 前处理,但最终仍返回对象。

易错点:一是多个组件共享同一对象引用;二是组件内直接使用父级数据;三是把 data 返回对象和实例 data 对象混为一谈。记住本节结论:组件中的 data 是函数,返回对象,且每次返回新对象。

先看现象:六个按钮为什么互不影响 ​

为什么需要:组件会被复用。同一个组件写多次,如果数据共用一份,点一个按钮就会把其他按钮一起改掉,这不符合组件“各自维护自己状态”的预期。
怎么想:先看笔记里的 App 和 BiaoGe。App 的 data 每次返回 { num: 1 },所以六个 <app> 各自拿到一个新对象;BiaoGe 的 data 返回外部 obj,六个 <biao-ge> 拿到的是同一个引用。
怎么做:在 App 的 data 里写 console.log("data"),页面上放六个 <app>,打开控制台会看到执行多次,说明“每使用一次组件,就调用一次组件的 data”。点击每个 App 按钮,数字各自加一,互不影响。
易错点:不要只看“写了函数”就以为独立。BiaoGe 也写了函数,但它 return obj,仍然共享。判断标准不是“有没有函数”,而是“每次调用是否返回新对象”。

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

为什么需要:Vue 根实例只有一个,根实例的 data 可以写成对象;但组件是模板,会被实例化多次,必须让每个组件实例拥有自己的数据。
怎么想:转写稿里反复强调“组件中的 data 必须是一个函数,且必须返回一个对象”。如果写成 data: { num: 1 },多个组件实例会围绕同一份数据工作,失去独立性。
怎么做:写成:

js
data() {
  return {
    num: 1
  }
}

易错点:两个错误最常见:一是 data 写成对象;二是 data(){} 忘记 return 对象。控制台会提示 data 应该返回一个对象。笔记代码里 App 是正确示范,BiaoGe 是反例示范。

从引用类型推导:返回 obj 为什么会“乱套” ​

为什么需要:JS 对象是引用类型。变量保存的是引用地址,不是每次复制一份内容。
怎么想:let obj = { num: 1 } 只创建了一个对象。BiaoGe 的 data(){ return obj } 每次返回同一个地址,所以多个 BiaoGe 实例共享 obj。点其中一个按钮,this.num++ 改的是同一个对象,其他按钮也跟着变。
怎么做:让 data 返回对象字面量,每次调用都创建新对象:

js
data() {
  return {
    num: 1
  }
}

这相当于工厂函数:每次调用生产一个“独一无二”的对象。转写稿里说的“单例模式”,在这里要理解为每个组件实例拿到独立对象,而不是全局共用一个对象。
易错点:data(){ return obj } 仍然是函数,但返回共享引用,依然错误;把对象提到外面再返回,也会共享。

函数形式还能放置逻辑 ​

为什么需要:有时数据初始化前要做判断、加工或读取配置。
怎么想:data 是函数,所以可以在 return 前写逻辑,最后返回对象。
怎么做:在 data() 内先计算,再 return { ... }。笔记注释也写了“可以设置逻辑程序”。
易错点:逻辑不能破坏“返回对象”的规则;不要把共享对象直接返回。

完整示例代码 ​

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>

调试与结论 ​

打开页面后重点对比两组按钮:上面六个 App 按钮各自加一,下面六个 BiaoGe 按钮会联动。再配合控制台里多次打印 "data",可以确认组件 data 被反复调用。结论是:组件中的 data 必须写成函数并返回对象;返回对象字面量,才能让每个组件实例的数据独立。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:组件嵌套(父组件内部再声明子组件) ​

html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>组件嵌套</title>
	<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
	<App></App>
</div>
</body>
<script>
	new Vue({
		el: "#app",
		// 在该 components 属性中声明的组件只允许在 Vue 实例中使用
		components: {
			// 声明了一个名字为 App 的组件
			App: {
				// 指定组件渲染内容
				template: `
					<div>
						<h3>App组件</h3>
						<Child></Child>
					</div>
				`,
				// 在该 components 属性中声明的组件只允许在组件 App 中使用
				components: {
					Child: {
						template: `
							<div>
								<h3>Child</h3>
							</div>
						`
					}
				}
			}
		}
	})
</script>
</html>

关键行与易错点

  • components 写在哪个配置对象里,声明的组件就只能在哪个作用域中使用:App 写在 new Vue 的 components 里,所以能在 #app 模板中使用;Child 写在 App 的 components 里,所以只能在 App 的 template 中使用。
  • template 必须只有一个根元素(这里是外层 <div>),写成并列多个标签会报错。
  • template 用反引号模板字符串书写,可换行;若用普通引号则不能换行。
  • 组件名在模板中使用时推荐 <App> 或 <app>,与注册名对应;不要漏写闭合标签。

示例二:组件中的 data 必须是函数,且返回独一无二的对象 ​

html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>组件中的 data</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 必须是函数(工厂函数),且函数必须 return 一个对象;写成 data: { num: 1 } 会直接报错。
  • App 组件每次使用都会执行一次 data(),返回的是字面量新对象,各实例数据互不影响,点谁谁加一。
  • BiaoGe 组件 return obj 返回的是同一个外部对象的引用,六个实例共享同一份数据,点一个全部变——这正是必须返回“独一无二对象”的原因。
  • 引用类型比较的是引用地址:let a = {} 与 let b = {} 不相等,而 let b = a 相等,所以复用一个外部对象就会造成数据共享。
  • 工厂函数里可以写逻辑代码(判断、计算后再 return),这也是函数形式的额外好处。

示例三:Vue 实例与组件实例的关系 ​

html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Vue实例与组件实例</title>
	<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
	<button @click="fn">点我</button>
	<hr/>
	<one></one>
</div>
</body>
<script>
	Vue.prototype.$useName = "zhangsan";
	const vm = new Vue({
		el: "#app",
		methods: {
			fn() {
				console.log(this === vm); // true
				console.log(this.__proto__ === Vue.prototype); // true
				console.log(vm.__proto__ === Vue.prototype); // true
				console.log(this.$useName)
			}
		},
		components: {
			// Vue 配置对象中有什么属性,那么组件配置对象中就可以设置什么属性
			one: {
				template: (`
					<button @click="fn2">one</button>
				`),
				methods: {
					fn2() {
						// this 是通过 VueComponent 实例化出来的
						// 组件实例与 Vue 实例的关联:this.__proto__.__proto__ === Vue.prototype
						console.log(this.__proto__); // Vue 实例
						console.log(this.__proto__.__proto__ === Vue.prototype);
						console.log(this.$useName)
					}
				}
			}
		}
	})
</script>
</html>

关键行与易错点

  • Vue 实例方法中的 this 就是 vm,其 __proto__ 指向 Vue.prototype,所以能访问原型上的 $useName。
  • 组件方法中的 this 是 VueComponent 的实例,不是 Vue 实例;它的 __proto__ 是 VueComponent.prototype,再往上一层 __proto__.__proto__ 才等于 Vue.prototype。
  • 因此组件里同样能拿到 this.$useName:自身实例找不到属性,就沿原型链向上查找。
  • 组件中不能直接调用父级(Vue 实例)methods 里定义的方法,fn 在组件内是未定义的,组件只能用自己 methods 中的 fn2。
  • 自定义属性建议用 $ 开头(如 $useName),这是命名约定而非语法强制。

说明:以上代码依赖同目录下 lib/vue.js,需保持笔记中的引入路径一致。

常见错误 ​

  1. 现象:子组件模板里直接写 {{ userName }},页面空白或控制台提示未定义。
    原因:组件作用域独立,父实例的 data 不会自动进入子组件。
    修法:父模板通过属性传值,子组件用 props 显式接收;或在子组件内部自行声明 data。

  2. 现象:组件 data 写成对象,或写成函数但没有 return 对象,控制台报错。
    原因:组件 data 必须是函数,且返回值必须是对象。
    修法:统一写成 data() { return { ... } }。

  3. 现象:多个相同组件实例点击其中一个按钮,其他实例数据跟着变化。
    原因:data() 返回了同一个外部对象引用,多个实例共享状态。
    修法:每次 return 新的对象字面量;需要预处理逻辑时,在 return 前计算,再返回新对象。

  4. 现象:父组件写 <user-card :user-name="name" />,子组件声明 props: ['username'] 却拿不到值。
    原因:属性名映射不匹配,驼峰与短横线命名未对应。
    修法:父模板用 kebab-case 传,子组件 props 用对应 camelCase 接收,或统一使用一致的短横线写法。

  5. 现象:子组件中执行 this.userName += '!',出现只读警告,父组件数据也没变。
    原因:props 是单向只读的,不能直接修改。
    修法:用本地 data/computed 派生展示值;确需修改时通过事件通知父组件修改源数据。

  6. 现象:组件能显示,但控制台警告 props 与 data/methods/filters 名称冲突。
    原因:同一组件实例命名空间中出现重名,来源不明确。
    修法:props、data、methods、filters、computed 中的名称统一规划,避免同名。

练习 ​

  1. 父组件有 msg: 'hello',子组件模板需要显示它。请写出父模板绑定、子组件 props 声明和子模板使用的关键代码;再说明如果父模板写成 msg="msg" 会得到什么结果。

  2. 下面写法会导致多个计数器组件联动,请改造为互不影响,并说明改造点在哪里:

    js
    const shared = &#123; num: 1 &#125;
    data() &#123; return shared &#125;
  3. 父组件使用 <user-card :user-name="name" :age="18" />,请写出子组件 props 的合理声明;并判断子组件中直接执行 this.age++ 是否合适,给出替代方案。

  4. 将内联 template 的 Child 组件抽离为 components/Child.js,请写出 export、import、局部注册、父模板使用的关键步骤。

  5. 子组件想使用父组件中定义的过滤器和方法。请判断能否直接使用,并给出两种合规做法。

原视频对照 ​

#视频时长
1组件示例08:51
2抽离组件08:57
3模块化14:07
4组件实例与Vue实例的关系09:30
5组件中的data24:52
6父向子传递参数17:57

小结 ​

  1. 组件边界就是作用域边界,跨组件数据必须显式传递。
  2. 局部注册只在当前注册者的模板范围内有效,注册位置要跟使用位置对应。
  3. 组件 data 必须用工厂函数返回新对象,核心目标是隔离实例状态。
  4. 父传子优先走 props;绑定表达式用 :,静态字符串不用 :。
  5. props 单向只读,修改需求应转为本地派生或事件上抛。
  6. props 与 data、methods、filters 等避免重名,减少调试成本。
  7. 抽离组件时重点检查 export/import、注册位置和模板来源是否一致。
  8. 组件实例共享 Vue.prototype 能力,但业务状态仍应各自声明、各自管理。