主题
为组件增加事件与全局组件(Lesson5 1/3)
本页对应 Lesson05,共 7 段视频,约 1 小时 58 分。
学习目标
- 能够将 todoList 页面拆分为头部、主体、底部组件,并将各组件抽离到独立文件中完成导入、导出与注册使用。
- 能够运用状态提升,将 todo 列表状态定义在父级实例中,并通过 props 与自定义事件完成兄弟组件间的数据传递和 todo 添加。
- 能够使用
$emit触发自定义事件,使用$on、$once、$off完成消息订阅、单次订阅与取消订阅,并说明发布者和订阅者需处于同一 Vue 实例。 - 能够通过
Vue.prototype.$bus创建事件总线,实现跨组件发布消息、订阅消息及携带参数传递数据。 - 能够为组件绑定事件,并在需要监听组件根元素原生事件时正确使用
.native修饰符,同时将处理函数定义在正确的组件实例中。 - 能够说明全局组件注册与动态组件的基本用法,并区分其与局部组件注册、普通组件使用的差异。
知识点
本节目标:让组件也能响应点击事件
这一节只解决一个具体问题:父组件使用子组件时,给子组件标签写 @click,为什么点击没有反应?笔记给出的答案是:给事件名加 .native 修饰符。转写稿里的调试过程很直接:先写 @click="fn",点击组件文字,控制台没有输出;改成 @click.native="fn" 后,再点击,控制台输出 fn。这说明普通 @click 并不是绑定到组件根元素的原生 DOM 事件上。
为什么需要:组件是自定义标签,Vue 默认把组件上的 @click 当成“组件自定义事件”来监听,而不是浏览器原生 click。
怎么想:我要的是点击组件根元素时触发父组件方法,所以要让 Vue 把 click 当作原生事件绑到组件根元素。
怎么做:在事件名后加 .native。
易错点:不要以为写了 @click 就一定能触发;函数要定义在父实例的 methods 里,因为事件写在父模板的组件标签上。
先看完整代码:@click.native 与 props 传递
笔记代码如下,保留原代码:
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">
<child @click.native="fn" :a="1" :styles="{color:'red'}" ></child>
</div>
</body>
<script>
// 如果要为组件提供事件,那么需要在事件名后增加修饰符native
new Vue({
el:"#app",
methods:{
fn(){
console.log("fn")
}
},
components:{
Child:{
props:["styles","a"],
template:(`
<div :style="styles">
child组件
</div>
`)
}
}
})
</script>
</html>这段代码里有两个知识点同时出现:第一,@click.native="fn" 给组件增加原生点击事件;第二,:a="1" 和 :styles="{color:'red'}" 是父组件向子组件传 props。子组件声明 props:["styles","a"],模板根元素写 :style="styles",所以文字会变成红色。a 在模板里没有使用,但可以作为“父传子属性”的观察点。
为什么需要:组件要复用,数据、样式不能都写死在子组件里,需要父组件传进去。
怎么想:父组件通过 : 绑定属性,子组件通过 props 接收,模板里再用。
怎么做:子组件声明 props:["styles","a"],根元素写 :style="styles"。
易错点:styles 传的是对象,不是字符串;props 名字要和绑定名一致;@click.native 的 fn 写在父实例 methods 中,不是写在子组件里。
调试过程:普通 @click 为什么没反应
按转写稿的演示,先把组件标签写成 @click="fn",点击“child组件”文字,控制台没有输出。原因不是 fn 写错,而是这个 @click 被当作组件自定义事件监听。组件没有通过 $emit('click') 派发事件,所以父组件的 fn 不会执行。
为什么需要:理解“原生事件”和“组件自定义事件”的区别,才能知道修饰符不是可有可无的。
怎么想:组件标签上的事件默认监听组件自己派发的事件;要监听根元素的原生 DOM 事件,需要显式声明。
怎么做:把 @click="fn" 改成 @click.native="fn",再点击组件根元素。
易错点:.native 是加在事件名后的,不是加在方法上;不要写成 @click="fn.native";Vue 2 中组件模板通常要求单根元素,.native 绑定的是这个根元素。
事件函数应该写在哪里
笔记里 fn 写在 new Vue 的 methods 中,因为 @click.native 写在父模板的 <child> 标签上,相当于父组件在监听子组件根元素。转写稿还提到另一种情况:如果事件直接写在子组件内部的按钮上,例如子组件模板里写 <button @click="fn">,那 fn 就应该定义在子组件的 methods 里。两者不要混。
为什么需要:事件写在哪个模板,处理函数通常就属于哪个组件实例。
怎么想:看 @click.native 写在父模板还是子模板:写在父模板组件标签上,函数放父 methods;写在子组件内部元素上,函数放子 methods。
怎么做:本节代码中 fn 放父实例 methods,控制台输出 fn。
易错点:把 fn 写进子组件 methods,父模板却用 @click.native="fn",会找不到方法;反过来也会出问题。
小结:记住“组件事件 + native”的边界
本节的核心结论是:给组件增加原生点击事件,需要在事件名后加 .native;同时,组件上的属性传递仍按 props 规则走。调试时先看控制台有没有输出,再检查事件写在哪一层、函数定义在哪一层、是否漏了 .native。不要把它和 $emit 自定义事件混为一谈:本节笔记只演示 .native 绑定原生事件,没有展开自定义事件派发。
全局组件要解决什么问题
为什么需要:有些组件会在多个页面、多个 Vue 实例甚至其他组件里反复出现。如果每个实例都写一遍 components 注册,既重复又容易漏。笔记注释已经点明:全局组件“可以在任意实例以及组件中使用的组件”。
怎么想:把“注册”从某个实例里拿出来,交给 Vue 本身。用 Vue.component(组件名, 配置项) 注册一次,之后在任意挂载模板里写对应标签即可使用。局部组件则只属于当前 new Vue 实例或当前父组件。
怎么做:笔记中先执行 Vue.component("one", {...})、Vue.component("two", {...}),再 new Vue({ el:"#app", components:{ Child:{...} } })。Child 是局部组件,但它的模板里直接写了 <one></one>,因为 one 已全局注册,所以在子组件模板中也能用。
易错点:Vue.component 只是“注册”,不会自动把组件塞到页面上;必须在模板里写 <one></one> 才会渲染。注册要写在 new Vue 使用之前。组件名和标签名要对应,实际项目里建议用短横线命名,减少大小写歧义。
从代码看配置项:data、methods、template
为什么需要:组件要维护自己的状态和交互,不能把状态写成所有实例共享的普通对象,否则一个组件改数据会影响另一个组件。
怎么想:组件的 data 必须是函数,函数返回一个新对象,这样每个组件实例拿到独立数据。methods 里通过 this 访问当前实例的数据。template 描述组件结构。
怎么做:笔记里 one 的 data(){ return { a:1, b:2 } },methods.fn 中执行 this.a++、this.b += 2;模板中绑定 {{a}}、{{b}},按钮绑定 @click="fn"。two 只有模板,也必须有根元素。
易错点:data 不要写成 data:{...} 普通对象;template 字符串里最好只有一个根元素,笔记用 <div> 包住 <h3>、<p>、<button>;事件方法名要和模板中一致。
调试过程:注册了为什么页面没出现
为什么需要:初学组件最常见的现象是“代码写了,页面没变化”。转写稿里就出现过类似调试:组件定义好后,页面上的头部没有出现,原因是“定义好了组件了,但是我们没有使用它”,需要在挂载容器模板里写组件标签,写完后效果才出来。
怎么想:把“定义组件”和“使用组件”分开检查。先看 Vue.component 是否在 new Vue 前执行;再看模板里有没有写标签;最后看标签名是否和注册名一致。
怎么做:笔记的 #app 中写了 <Child></Child>、<one></one>、<two></two>。Child 内部又写了 <one></one>。这样 one 在根实例和局部组件 Child 中都能渲染。
易错点:只注册不写标签;标签拼错;把全局组件误以为必须写进 components;在 #app 外写组件标签,导致 Vue 不接管。
全局组件与局部组件的边界
为什么需要:全局注册方便,但所有地方都能用会让依赖不清晰;局部注册更可控。转写稿讲组件拆分时也强调把头部、主体、底部拆成组件,抽离后仍要明确“在哪里注册、在哪里使用”。
怎么想:通用基础组件可全局注册;页面专用组件放当前实例的 components。笔记中 one、two 是全局,Child 是局部,正好对比。
怎么做:全局用 Vue.component("one", {...});局部用 components:{ Child:{ template:... } }。局部组件只在当前实例模板中可用;全局组件可在任意实例及组件模板中使用。
易错点:把局部组件拿到另一个实例中用会找不到;全局组件过多会增加命名冲突风险;Child 这种大写开头在 HTML 模板中要留意大小写转换,建议统一短横线命名。
本节完整代码
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">
<Child></Child>
<one></one>
<two></two>
</div>
</body>
<script>
// 全局组件:可以在任意实例以及组件中使用的组件
// 第一个参数是组件名,第二个参数是组件的配置项
Vue.component("one",{
data(){
return {
a:1,
b:2
}
},
methods:{
fn(){
this.a ++;
this.b +=2;
}
},
template:(`
<div>
<h3>one</h3>
<p>a:{{a}}</p>
<p>b:{{b}}</p>
<button @click="fn">点击</button>
</div>
`)
});
Vue.component("two",{
template:(`
<div>
<h3>two</h3>
</div>
`)
})
new Vue({
el:"#app",
components:{
Child:{
template:(`
<div>
<h3>Child</h3>
<one></one>
</div>
`)
}
}
})
</script>
</html>检查时按顺序看:全局注册是否在 new Vue 前;#app 内是否写了 <one>、<two>;Child 内是否也能写 <one>;data 是否为函数;每个 template 是否只有一个根元素。这样就能把本节“全局组件”的注册、使用和调试过程串起来。
先看痛点:为什么需要动态组件
为什么需要:页面上要在多个组件之间切换时,最容易写成 v-if / v-else-if 一条链。本节笔记里被注释掉的代码就是这种写法:<one v-if="index===0">、<two v-else-if="index===1">……组件一多,模板会越来越长,新增一个组件就要再加一个分支,维护成本高。转写稿里反复强调组件拆分后代码阅读性会变差,动态组件解决的是同一类问题:不要把“当前显示哪个组件”写死在模板里。
怎么想:把“当前用哪个组件”抽成一个数据状态,例如 index;再把组件名放进数组 arr。模板只写一个内置组件 <component>,用 :is 绑定 arr[index]。这样模板结构不变,变的是数据。
怎么做:先注册四个组件 One/Two/Three/Four,再准备 arr:["one","two","three","four"] 和 index:0,模板写 <component :is="arr[index]"></component>。
易错点:is 前必须有冒号,表示绑定表达式;如果写成 is="arr[index]",会被当成普通字符串。arr 里的名字要与组件注册名能对应上;笔记里注册的是 One,数组写的是小写 "one",Vue 会做名称解析,但实际项目更建议统一命名风格。
核心写法:component + is 把组件名数据化
为什么需要:动态组件的关键不是“多写一个标签”,而是把组件切换变成数据驱动。转写稿里讲 todoList 时提到“视图通过数据来驱动”,这里同样适用:index 变,arr[index] 变,渲染的组件就变。
怎么想:component 是 Vue 内置组件,is 的值告诉它“要渲染哪个组件”。这个值可以是组件名字符串,也可以是组件对象。本节用字符串数组,最适合做轮播式切换。
怎么做:保留下面完整代码,重点看模板中的 <component :is="arr[index]"></component> 和 changeIndex 的自增归零逻辑。
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="changeIndex">切换</button>
<!--使用动态组件:component是内置组件,is属性的值是要使用的组件的名字-->
<component :is="arr[index]"></component>
<!-- <component v-show="index===i" v-for="(item,i) in arr" :is="item" :key="i"></component>-->
<!--未使用动态组件-->
<!-- <one v-if="index===0"></one>-->
<!-- <two v-else-if="index===1"></two>-->
<!-- <three v-else-if="index===2"></three>-->
<!-- <four v-else-if="index===3"></four>-->
</div>
</body>
<script>
new Vue({
el:"#app",
data:{
arr:["one","two","three","four"],
index:0
},
methods:{
changeIndex(){
this.index++;
if(this.index>3) this.index=0;
}
},
components:{
One:{
template:(`
<div style="background:red">one</div>
`)
},
Two:{
template:(`
<div style="background:green">two</div>
`)
},
Three:{
template:(`
<div style="background:yellow">three</div>
`)
},
Four:{
template:(`
<div style="background:pink">four</div>
`)
}
}
})
</script>
</html>易错点:changeIndex 里 if(this.index>3) this.index=0; 是循环切换的关键;不归零会越界,arr[index] 变成 undefined,页面就不渲染目标组件。组件模板必须只有一个根元素,笔记里每个模板都是一个 div。
切换与调试:从 v-if 链改成动态组件的验证过程
为什么需要:只看代码容易以为“能切换”,实际要验证 index 是否变化、is 是否取到正确组件名、组件是否注册成功。
怎么想:转写稿里做 todoList 时先拆组件、再抽离文件、再传参,调试思路是“先确认数据状态,再确认视图”。动态组件也一样:先点按钮看 index 是否 0→1→2→3→0,再看 arr[index] 是否对应 one/two/three/four,最后看渲染出的背景色和文字是否变化。
怎么做:打开控制台或在 changeIndex 里临时打印 this.index;点击“切换”按钮,观察红、绿、黄、粉四个块依次出现。若不变,依次检查:@click 是否绑定、方法名是否一致、components 是否注册、:is 是否漏冒号、数组值是否写错。
易错点:不要同时保留注释里的 v-if 链和动态组件,否则会出现重复渲染或干扰判断。笔记中另一条 v-show + v-for 方案是把所有组件都渲染出来再控制显示,和 component :is 的“只渲染当前组件”思路不同,初学阶段先用本节主写法。
组件注册与命名:动态组件的“名字”从哪来
为什么需要::is 绑定的是“组件名”,名字对不上就找不到组件。转写稿里把组件抽到 com 目录时强调导出、引入、注册要一致,动态组件同样依赖注册关系。
怎么想:本节采用局部注册:components:{ One:{...}, Two:{...} }。arr 中存的是查找组件的键。模板里只写一次 <component>,由 Vue 根据 is 的值去 components 里找。
怎么做:保持 arr 与 components 的键对应;如果改成全局注册,也要保证 Vue.component('one', {...}) 的名字与 arr 一致。实际项目建议统一用 kebab-case 或 PascalCase,避免大小写混乱。
易错点:组件名不是模板里的文字内容,template 里的 one/two 只是显示文本;真正决定渲染哪个组件的是 components 的键和 arr[index]。另外,动态组件切换时组件实例会变化,若组件内部有输入状态,不能想当然认为它会自动保留,需要结合后续 keep-alive 等知识再处理。
课堂画面






代码示例
示例一:为组件增加事件(.native 修饰符)
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">
<child @click.native="fn" :a="1" :styles="{color:'red'}"></child>
</div>
</body>
<script>
// 如果要为组件提供事件,那么需要在事件名后增加修饰符 native
new Vue({
el: "#app",
methods: {
fn() {
console.log("fn")
}
},
components: {
Child: {
props: ["styles", "a"],
template: (`
<div :style="styles">
child组件
</div>
`)
}
}
})
</script>
</html>关键行与易错点
@click.native="fn":@click监听的是“组件自定义事件”,组件内部没有$emit('click'),所以事件不会触发;加.native后才会把原生 DOM 的 click 事件绑定到组件根元素上。fn必须定义在使用组件的那一层(这里是根实例)的methods里,写在子组件里是接不到的。:a="1"传数字、:styles="{color:'red'}"传对象,都需要:(v-bind),否则传过去的是字符串"1"、"{color:'red'}"。- 子组件必须用
props: ["styles","a"]声明接收,否则:style="styles"拿不到值。 - 组件名
Child写在 HTML 模板里会被浏览器转成小写child,Vue 内部会做驼峰匹配,所以能生效;但命名上建议统一用短横线写法,避免歧义。
示例二:全局组件
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">
<Child></Child>
<one></one>
<two></two>
</div>
</body>
<script>
// 全局组件:可以在任意实例以及组件中使用的组件
// 第一个参数是组件名,第二个参数是组件的配置项
Vue.component("one", {
data() {
return {
a: 1,
b: 2
}
},
methods: {
fn() {
this.a++;
this.b += 2;
}
},
template: (`
<div>
<h3>one</h3>
<p>a:{{a}}</p>
<p>b:{{b}}</p>
<button @click="fn">点击</button>
</div>
`)
});
Vue.component("two", {
template: (`
<div>
<h3>two</h3>
</div>
`)
})
new Vue({
el: "#app",
components: {
Child: {
template: (`
<div>
<h3>Child</h3>
<one></one>
</div>
`)
}
}
})
</script>
</html>关键行与易错点
Vue.component("one", {...})必须在new Vue({...})之前注册,否则模板编译时找不到组件。- 全局组件
one既能直接在#app里用,也能在局部组件Child的模板里用——这正是“全局”的含义;Child是局部组件,只能在#app内使用。 - 组件的
data必须是函数并return一个对象(data(){ return {a:1,b:2} }),这样每个组件实例都有独立数据;若写成对象字面量,多个实例会共享同一份数据。 this.a++是在组件自己的实例上操作,不会影响别的one实例。template要求只有一个根元素,所以one里的<h3>、<p>、<button>都被包在一个<div>里,否则会报错。
示例三:动态组件
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="changeIndex">切换</button>
<!--使用动态组件:component是内置组件,is属性的值是要使用的组件的名字-->
<component :is="arr[index]"></component>
<!-- <component v-show="index===i" v-for="(item,i) in arr" :is="item" :key="i"></component>-->
<!--未使用动态组件-->
<!-- <one v-if="index===0"></one>-->
<!-- <two v-else-if="index===1"></two>-->
<!-- <three v-else-if="index===2"></three>-->
<!-- <four v-else-if="index===3"></four>-->
</div>
</body>
<script>
new Vue({
el: "#app",
data: {
arr: ["one", "two", "three", "four"],
index: 0
},
methods: {
changeIndex() {
this.index++;
if (this.index > 3) this.index = 0;
}
},
components: {
One: {
template: (`
<div style="background:red">one</div>
`)
},
Two: {
template: (`
<div style="background:green">two</div>
`)
},
Three: {
template: (`
<div style="background:yellow">three</div>
`)
},
Four: {
template: (`
<div style="background:pink">four</div>
`)
}
}
})
</script>
</html>关键行与易错点
<component :is="arr[index]">中component是 Vue 内置组件,is的值是“要渲染的组件名”,index一变,渲染的组件就跟着变,不用写一堆v-if / v-else-if。is绑定的名字要能和注册名对上:arr里写"one",注册的是One,Vue 会做驼峰匹配;如果名字对不上(如写成"one1"),会渲染失败或当作普通标签。index必须在data里定义,页面才能响应changeIndex里对this.index的修改。if (this.index > 3) this.index = 0;是循环切换的边界处理,少了这句,arr[index]会取到undefined。- 每次切换都会销毁旧组件、创建新组件,组件内部状态不会保留;如果需要保留状态,要用
<keep-alive>包裹(本节未展开)。
常见错误
- 现象:给组件写
@click="fn"点击无反应 → 原因:未绑定到组件根元素原生事件 → 修法:改为@click.native="fn"。 - 现象:子组件里
this.$emit('add'),父实例的$on('add')不触发 → 原因:$emit与$on不在同一 Vue 实例 → 修法:统一挂到Vue.prototype.$bus = new Vue()后,用this.$bus.$emit/$on。 - 现象:先发布消息,后注册订阅,回调不执行 → 原因:发布时还没有订阅者 → 修法:先
$on订阅,再$emit发布。 - 现象:
$once注册后第二次发布无响应 → 原因:$once只响应一次并自动移除 → 修法:需要多次响应改用$on。 - 现象:子组件直接
this.todoList = [...]报错或失效 → 原因:直接覆盖 props → 修法:通过$emit通知父级,由父级更新状态。 - 现象:动态组件
<component :is="arr[index]">不渲染 → 原因:is值不是已注册组件名或大小写不一致 → 修法:确认组件已注册,is使用对应组件名字符串。 - 现象:抽离组件后页面空白/报未定义 → 原因:
export default、import名称或路径不一致 → 修法:核对导出方式与引入名称,保持文件名和组件名一致。 - 现象:
mounted中拿不到预期 DOM 内容 → 原因:挂载完成时机理解错误,子组件mounted先于父实例 → 修法:在对应组件自身mounted中取 DOM,父级逻辑放到父mounted。
练习
- 给
child组件根元素绑定点击事件,分别用不加.native和加.native测试,并说明差异。 - 全局注册
one、two,在局部组件Child中同时使用,再在根实例中使用,检查注册名与标签名写法。 - 用
<component :is="arr[index]">实现四个组件循环切换,并用v-if/v-else-if写一版对比代码量。 - 完成 todo 添加:父级定义
todoList,头部组件$emit提交标题,父级校验空值和重名后更新,主体组件通过 props 渲染。 - 用
Vue.prototype.$bus实现两个兄弟组件通信:A 发布消息,B 订阅;再分别验证$once只执行一次、$off后不再响应。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 完成todoList组件拆分 | 14:37 |
| 2 | 将组件放置到外部 | 07:30 |
| 3 | 完成todo添加 | 27:49 |
| 4 | 完成todo添加2 | 08:05 |
| 5 | $emit $on $once $off | 27:40 |
| 6 | 事件总线 | 22:56 |
| 7 | 为组件增加事件 | 09:42 |
小结
- 组件根元素原生事件要用
.native,普通元素事件不需要。 - 全局组件先注册后使用,标签名建议用 kebab-case。
- 动态组件核心是
:is,值应是已注册组件名。 - 兄弟组件共享状态优先放到共同父级,即状态提升。
- 子组件不能直接覆盖 props,修改共享状态要通过
$emit交给父级。 $emit/$on/$once/$off必须基于同一实例;跨组件常用事件总线统一实例。- 事件总线要先订阅后发布;
$once只响应一次,$off用于取消订阅。 - 组件抽离后重点检查导出/导入、注册名、props 接收名三者一致。