主题
组件嵌套与组件示例(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: `...` },在 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 必须写成函数并返回对象;返回对象字面量,才能让每个组件实例的数据独立。
课堂画面






代码示例
示例一:组件嵌套(父组件内部再声明子组件)
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,需保持笔记中的引入路径一致。
常见错误
现象:子组件模板里直接写
{{ userName }},页面空白或控制台提示未定义。
原因:组件作用域独立,父实例的data不会自动进入子组件。
修法:父模板通过属性传值,子组件用props显式接收;或在子组件内部自行声明data。现象:组件
data写成对象,或写成函数但没有return对象,控制台报错。
原因:组件data必须是函数,且返回值必须是对象。
修法:统一写成data() { return { ... } }。现象:多个相同组件实例点击其中一个按钮,其他实例数据跟着变化。
原因:data()返回了同一个外部对象引用,多个实例共享状态。
修法:每次return新的对象字面量;需要预处理逻辑时,在return前计算,再返回新对象。现象:父组件写
<user-card :user-name="name" />,子组件声明props: ['username']却拿不到值。
原因:属性名映射不匹配,驼峰与短横线命名未对应。
修法:父模板用kebab-case传,子组件props用对应camelCase接收,或统一使用一致的短横线写法。现象:子组件中执行
this.userName += '!',出现只读警告,父组件数据也没变。
原因:props是单向只读的,不能直接修改。
修法:用本地data/computed派生展示值;确需修改时通过事件通知父组件修改源数据。现象:组件能显示,但控制台警告
props与data/methods/filters名称冲突。
原因:同一组件实例命名空间中出现重名,来源不明确。
修法:props、data、methods、filters、computed中的名称统一规划,避免同名。
练习
父组件有
msg: 'hello',子组件模板需要显示它。请写出父模板绑定、子组件props声明和子模板使用的关键代码;再说明如果父模板写成msg="msg"会得到什么结果。下面写法会导致多个计数器组件联动,请改造为互不影响,并说明改造点在哪里:
jsconst shared = { num: 1 } data() { return shared }父组件使用
<user-card :user-name="name" :age="18" />,请写出子组件props的合理声明;并判断子组件中直接执行this.age++是否合适,给出替代方案。将内联
template的Child组件抽离为components/Child.js,请写出export、import、局部注册、父模板使用的关键步骤。子组件想使用父组件中定义的过滤器和方法。请判断能否直接使用,并给出两种合规做法。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 组件示例 | 08:51 |
| 2 | 抽离组件 | 08:57 |
| 3 | 模块化 | 14:07 |
| 4 | 组件实例与Vue实例的关系 | 09:30 |
| 5 | 组件中的data | 24:52 |
| 6 | 父向子传递参数 | 17:57 |
小结
- 组件边界就是作用域边界,跨组件数据必须显式传递。
- 局部注册只在当前注册者的模板范围内有效,注册位置要跟使用位置对应。
- 组件
data必须用工厂函数返回新对象,核心目标是隔离实例状态。 - 父传子优先走
props;绑定表达式用:,静态字符串不用:。 props单向只读,修改需求应转为本地派生或事件上抛。props与data、methods、filters等避免重名,减少调试成本。- 抽离组件时重点检查
export/import、注册位置和模板来源是否一致。 - 组件实例共享
Vue.prototype能力,但业务状态仍应各自声明、各自管理。