Vue:组件的使用
1、什么是组件
1.1、传统方式开发的应用
一个网页通常包括三部分:结构(HTML)、样式(CSS)、交互(JavaScript)。在传统开发模式下,随着项目规模的增大,代码结构会变得愈发复杂。例如,多个页面可能共享部分样式和交互逻辑,但代码却分散在不同文件中,维护和修改时需要在多个地方查找相关代码,不仅效率低下,还容易引发错误。
1.2、组件化方式开发的应用
使用组件化方式开发解决了以上的两个问题:
① 每一个组件都有独立的js,独立的css,这些独立的js和css只供当前组件使用,不存在纵横交错。更加便于维护。以一个电商项目为例,商品展示组件有自己独立的JavaScript代码来处理商品数据展示逻辑,CSS样式只作用于该组件内的元素,与其他组件如购物车组件、用户登录组件等相互独立,当需要修改商品展示样式或逻辑时,只需要专注于该组件内部代码,不会影响到其他组件。
② 代码复用性增强。组件不仅让js、css复用了,HTML代码片段也复用了(因为要使用组件直接引入组件即可)。在项目中,像导航栏、按钮等组件,在多个页面或不同位置可能会重复使用。通过组件化,只需要编写一次这些组件代码,然后在需要的地方引入,大大提高了开发效率。
1.3、什么是组件?
① 组件:实现应用中局部功能的代码和资源的集合。凡是采用组件方式开发的应用都可以称为组件化应用。在Vue中,一个简单的按钮组件可能包含按钮的HTML结构、样式以及点击事件处理的JavaScript代码,这些代码和资源组合在一起,实现了按钮的功能,并且可以在不同地方复用。
② 模块:一个大的js文件按照模块化拆分规则进行拆分,生成多个js文件,每一个js文件叫做模块。凡是采用模块方式开发的应用都可以称为模块化应用。例如,在一个大型JavaScript项目中,将数据请求功能、数据处理功能等分别拆分成不同的模块,每个模块专注于特定功能,提高代码的可维护性和可复用性。
③ 任何一个组件中都可以包含这些资源:HTML、CSS、JS、图片、声音、视频等。从这个角度也可以说明组件是可以包括模块的。比如一个视频播放组件,除了HTML结构和CSS样式来定义播放界面,JavaScript代码来控制播放逻辑外,还可能包含视频资源文件以及用于播放控制的图片图标等。
1.4、组件的划分粒度很重要
粒度太粗会影响复用性。为了让复用性更强,Vue的组件也支持父子组件嵌套使用。例如,在一个复杂的页面布局中,将页面划分为头部、主体、底部等大粒度组件后,主体部分又可以进一步细分为文章列表组件、推荐组件等子组件。子组件由父组件来管理,父组件由父组件的父组件管理。在Vue中根组件就是vm。因此每一个组件也是一个Vue实例。这意味着每个组件都有自己独立的生命周期、数据和方法,它们之间通过特定的通信机制进行交互,构建出复杂而有序的应用结构。
2、组件的创建,注册,使用
2.1、组件的创建、注册、局部使用
第一步:创建组件
Vue.extend({该配置项和new Vue的配置项几乎相同,略有差别})
区别有哪些?
- 创建Vue组件的时候,配置项中不能使用el配置项。因为组件具有通用性,不特定为某个容器服务,它为所有容器服务。如果组件中指定了el,就限制了其只能在特定的DOM元素下使用,无法在其他地方复用。
- 配置项中的data不能使用直接对象的形式,必须使用function。以保证数据之间不会相互影响。例如,在多个相同组件实例存在的情况下,若data为对象,一个实例修改了数据,其他实例也会受到影响;而使用函数形式,每个实例都会有自己独立的数据副本。
3、使用template配置项来配置模板语句:HTML结构。通过template可以清晰地定义组件的HTML结构,使其与JavaScript和CSS分离,增强代码的可读性和维护性。
第二步:注册组件
局部注册:
在配置项当中使用components,语法格式:
components : {
组件的名字 : 组件对象
}
全局注册:
Vue.component(‘组件的名字’, 组件对象)。全局注册的组件可以在任何Vue实例的模板中使用,无需在每个组件内部再次注册。但在大型项目中,过多的全局注册组件可能会导致命名冲突和代码难以维护,因此局部注册在很多场景下更具优势。
第三步:使用组件
1、直接在页面中写<组件的名字></组件的名字>
2、也可以使用单标签<组件的名字 />。这种方式一般在脚手架中使用,否则会有元素不渲染的问题。在非脚手架环境中,某些浏览器可能不支持单标签自闭合语法,导致组件无法正确渲染,所以建议在非脚手架环境中使用双标签形式。
补充:组件的使用分为三步:
第一步:创建组件
Vue.extend({该配置项和new Vue的配置项几乎相同,略有差别})。
第二步:注册组件
局部注册:
在配置项当中使用components,语法格式:
components : {
组件的名字 : 组件对象
}
全局注册:
Vue.component(‘组件的名字’, 组件对象)
第三步:使用组件
<body><div id="app"><h1>{{msg}}</h1><!-- 3. 使用组件 --><userlist></userlist></div><script>// 1.创建组件(结构HTML 交互JS 样式CSS)const myComponent = Vue.extend({template: `<ul><li v-for="(user,index) of users" :key="user.id">{{index}},{{user.name}}</li></ul>`,data() {return {users: [{ id: "001", name: "jack" },{ id: "002", name: "lucy" },{ id: "003", name: "james" },],};},});// Vue实例const vm = new Vue({el: "#app",data: {msg: "第一个组件",},// 2. 注册组件(局部注册)components: {// userlist是组件的名字。myComponent只是一个变量名。userlist: myComponent,},});</script>
</body>
2.2、为什么组件中data数据要使用函数形式
面试题:为什么组件中data数据要使用函数形式
在 Vue 组件中 data
使用函数形式,原因有三:
一是保证组件复用性,若 data
为对象,复用实例会共享数据,修改一处影响其他实例;函数形式能让各实例有独立 data
副本,数据互不干扰。比如在一个商品列表页面,每个商品项都是一个组件实例,如果data为对象,当修改一个商品的价格时,其他商品的价格也会跟着改变,这显然不符合预期;而使用函数形式,每个商品组件实例都有自己独立的价格数据。
二是实现数据隔离与安全,降低数据冲突和意外修改风险,提升代码可维护性。不同组件实例的数据相互独立,避免了因一个实例的数据修改影响其他实例,使得代码在维护和扩展时更加稳定可靠。
三是契合 Vue 设计理念,使组件更独立、遵循单一职责原则。每个组件专注于自己的功能和数据,提高了代码的模块化程度。
<script>// 数据只有一份,数据会互相影响let dataobj = {counter: 1,};let a = dataobj;let b = dataobj;function datafun() {return {counter: 1,};}// 只要运行一次函数,就会创建一个全新的数据,互不影响let x = datafun();let y = datafun();
</script>
2.3、创建组件对象的简写方式
创建组件对象也有简写形式:Vue.extend() 可以省略。直接写:{}。Vue在内部会自动处理,当使用这种简写方式注册组件时,它会将对象作为参数传递给Vue.extend() 进行组件创建。这种简写方式使代码更加简洁,提高了开发效率。
<body><div id="app"><h1>{{msg}}</h1><!-- 3. 使用组件 --><userlogin></userlogin></div><script>// 1. 创建组件/* const userLoginComponent = Vue.extend({template : `<div><h3>用户登录</h3><form @submit.prevent="login">账号:<input type="text" v-model="username"> <br><br>密码:<input type="password" v-model="password"> <br><br><button>登录</button></form></div>`,data(){return {username : '',password : ''}},methods: {login(){alert(this.username + "," + this.password)}},}) */// 底层会在局部或全局注册组件时,自动调用Vue.extend()const userLoginComponent = {template: `<div><h3>用户登录</h3><form @submit.prevent="login">账号:<input type="text" v-model="username"> <br><br>密码:<input type="password" v-model="password"> <br><br><button>登录</button></form></div>`,data() {return {username: "",password: "",};},methods: {login() {alert(this.username + "," + this.password);}},};// Vue实例const vm = new Vue({el: "#app",data: {msg: "第二个用户登录组件",},// 2. 注册组件(局部注册)components: {userlogin: userLoginComponent,},});</script>
</body>
2.4、组件的全局注册
<body><!-- 组件的使用分为三步:第一步:创建组件Vue.extend({该配置项和new Vue的配置项几乎相同,略有差别})。第二步:注册组件局部注册:在配置项当中使用components,语法格式:components : {组件的名字 : 组件对象}全局注册:Vue.component('组件的名字', 组件对象)第三步:使用组件 --><div id="app"><h1>{{msg}}</h1><!-- 3. 使用组件 --><userlogin></userlogin></div><hr /><div id="app2"><userlogin></userlogin></div><script>const userLoginComponent = {template: `<div><h3>用户登录</h3><form @submit.prevent="login">账号:<input type="text" v-model="username"> <br><br>密码:<input type="password" v-model="password"> <br><br><button>登录</button></form></div>`,data() {return {username: "",password: "",};},methods: {login() {alert(this.username + "," + this.password);}},};// 全局注册Vue.component("userlogin", userLoginComponent);// 第2个vue实例const vm2 = new Vue({el: "#app2",});// Vue实例const vm = new Vue({el: "#app",data: {msg: "全局注册组件",},// 注册组件(局部注册)// components: {// userlogin : userLoginComponent// },});</script>
</body>
全局注册的组件在整个应用中都可使用,方便在不同的Vue实例中复用。但在大型项目中,要注意组件命名的唯一性,避免不同模块中同名组件冲突。例如,在一个包含多个功能模块的项目中,若两个模块都定义了名为“button”的组件并全局注册,就会导致命名冲突,使应用出现不可预期的错误。
2.5、组件的命名细节
注册组件细节:
- 在Vue当中是可以使用自闭合标签的,如果组件需要多次使用,前提必须在脚手架环境中使用。在非脚手架环境下,一些浏览器可能不识别自闭合标签,导致组件渲染异常。例如在IE浏览器的某些版本中,使用自闭合标签可能会使组件无法正常显示内容。
- 在创建组件的时候Vue.extend()可以省略,但是底层实际上还是会调用的,在注册组件的时候会调用。这一特性使得代码书写更加简洁,开发者无需每次都显式调用Vue.extend(),提高了开发效率。
- 组件的名字
(1):全部小写。这种命名方式简单直观,在HTML模板中使用时符合HTML标签的命名习惯,例如<my - component>
。
(2):首字母大写,后面都是小写。如<MyComponent>
,在一些代码风格规范中常用于区分组件和普通HTML标签。
(3):kebab - case命名法(串式命名法。例如:user - login)。这是一种常用的命名方式,在HTML模板中可读性强,易于理解组件的功能。
(4):CamelCase命名法(驼峰式命名法。例如:UserLogin)。但是这种方式只允许在脚手架环境中使用。在脚手架项目中,使用驼峰式命名法可以使组件名在JavaScript代码中更符合编程习惯,同时在模板中通过特定配置也能正确识别。
(5)不要使用HTML内置的标签名作为组件的名字。例如:header,main,footer。使用HTML内置标签名作为组件名可能会导致混淆和冲突,使浏览器无法正确解析和渲染组件。
(6)在创建组件的时候,通过配置项配置一个name,这个name不是组件的名字,是设置Vue开发者工具中显示的组件的名字。例如,在调试复杂应用时,通过设置name可以更方便地在开发者工具中识别和查找组件,提高调试效率。
<body><div id="app"><h1>{{msg}}</h1><!-- 3. 使用组件 --><hello - world></hello - world><hello - world /><!-- 使用多个的时候,会报错 --><!-- <hello - world /><hello - world /> --></div><script>// 1、创建组件// const hello = {// template: `<h1> helloworld </h1>`,// };// 2、全局注册组件// Vue.component("hello - world", hello);// 注册的时候,同时创建组件Vue.component("hello - world", {name: "hw",template: `<h1> HelloWorld </h1>`,});// Vue实例const vm = new Vue({el: "#app",data: {msg: "组件注册注意点",},});</script>
</body>
3、组件的嵌套
哪里要使用,就到哪里去注册,去使用。组件嵌套是构建复杂页面结构的重要方式。通过合理的组件嵌套,可以将一个大型页面拆分成多个层次分明、功能独立的组件。例如,在一个电商商品详情页面中,可能会有商品基本信息组件、商品图片展示组件、商品评论组件等,这些组件又可能各自包含子组件,如商品图片展示组件中可能包含图片切换子组件、图片预览子组件等。
在父组件中注册并使用子组件时,要注意组件的作用域和通信问题。父组件可以通过props向子组件传递数据,子组件可以通过 $ emit触发事件向父组件传递信息。比如在一个包含商品列表组件(父组件)和单个商品展示组件(子组件)的场景中,父组件可以将商品数据列表通过props传递给子组件,子组件在用户点击商品详情按钮时,通过$emit触发一个自定义事件,通知父组件进行相应的操作,如跳转到商品详情页。
在实际项目开发中,合理规划组件嵌套的层级非常关键。过深的嵌套层级可能会导致组件间通信变得复杂,增加维护成本。一般建议将嵌套层级控制在3 - 4层以内,若超过这个范围,可以考虑通过状态管理工具(如Vuex)来简化组件间的数据传递和共享。
当父组件更新时,会触发子组件的更新生命周期钩子函数。子组件可以在beforeUpdate
和updated
钩子函数中,根据父组件传递过来的新数据进行相应的操作,比如重新计算某些依赖数据、更新DOM元素等。
在组件嵌套的场景下,调试也需要一些技巧。当发现页面显示异常或功能错误时,可以利用Vue开发者工具,通过组件树来快速定位到可能出现问题的组件层级。可以查看每个组件的props数据、data状态以及事件触发情况,从而更高效地排查问题。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF - 8" /><title>组件嵌套</title><script src="../js/vue.js"></script>
</head>
<body><div id="root"><app></app></div><script>//4创建child组件const child = {template: `<h3>child组件</h3>`,};//3创建girl组件const girl = {template: `<h2>girl组件</h2>`,};//2 创建son组件const son = {template: `<div><h2>son组件</h2><child /></div>`,components: {child,},mounted() {// 可以在这里访问子组件实例const childInstance = this.$children[0];console.log('子组件child实例:', childInstance);}};//1、创建app组件,并注册son组件和girl组件const app = {template: `<div><h1>app组件</h1><girl /><son /></div>`,components: {girl,son,},updated() {console.log('app组件更新了');}};// 创建vm,并注册app组件const vm = new Vue({el: "#root",// 1.3 使用app组件//1.2、 注册app组件components: {app,},data() {return {// 可以在这里定义一些共享数据,通过props传递给子组件sharedData: '这是来自根组件的数据'};}});</script>
</body>
在上述代码中,app
组件作为最外层组件,嵌套了girl
和son
组件,而son
组件又嵌套了child
组件。通过在组件的生命周期钩子函数中添加日志输出,可以清晰地看到组件的加载和更新顺序。在son
组件的mounted
钩子函数中,可以通过this.$children
来访问子组件实例,实现父子组件间更直接的交互。同时,app
组件中的updated
钩子函数可以在组件数据更新时触发,用于执行一些与更新相关的逻辑。
4、VueComponent & Vue
4.1、this
new Vue({})
配置项中的this
就是:Vue实例(vm)。在这个实例中,可以访问data
中的数据、调用methods
中的方法,并且其生命周期钩子函数也围绕着整个Vue应用的创建、挂载、更新和销毁过程执行。例如,在mounted
钩子函数中,可以操作DOM元素,因为此时Vue实例已经挂载到了页面上。
Vue.extend({})
配置项中的this
就是:VueComponent实例(vc)。每个通过Vue.extend
创建的组件都是一个VueComponent
实例。它也有自己的data
、methods
和生命周期钩子函数,但与Vue实例不同的是,它主要用于构建可复用的组件模块。比如在一个组件的created
钩子函数中,可以进行一些组件特定的数据初始化操作,而不会影响到其他组件。
打开vm和vc你会发现,它们拥有大量相同的属性。例如:生命周期钩子、methods、watch等。这是因为VueComponent
的设计目的就是为了复用Vue的基本功能,同时又能保持组件的独立性和可定制性。例如,无论是Vue实例还是VueComponent
实例,都可以通过watch
来监听数据的变化,并在数据变化时执行相应的逻辑。
<body><div id="app"><h1>{{msg}}</h1><user></user></div><script>// 创建组件const user = Vue.extend({template: `<div><h1>user组件</h1></div>`,mounted() {// user是什么呢????是一个全新的构造函数 VueComponent构造函数。// this是VueComponent实例console.log('vc', this)},});// vmconst vm = new Vue({el: "#app",data: {msg: "vm与vc",},components: {user,},mounted() {// this是Vue实例console.log("vm", this);},});</script>
</body>
在上述代码中,在user
组件(VueComponent
实例)的mounted
钩子函数中输出this
,可以看到其指向的是组件自身的实例,包含了组件特有的属性和方法。而在Vue实例的mounted
钩子函数中输出this
,则指向整个Vue应用的实例,包含了应用级别的数据和配置。
4.2 vm === vc ???
只能说差不多一样,不是完全相等。
例如:
vm上有el
,vc上没有。因为el
用于指定Vue实例挂载的DOM元素,而组件本身是可复用的,不应该固定在某个特定的DOM元素上。
另外data
也是不一样的。vc的data
必须是一个函数。这是为了保证每个组件实例都有独立的数据副本,避免数据共享导致的问题。
只能这么说:vm上有的vc上不一定有,vc上有的vm上一定有。因为VueComponent
继承了Vue的部分特性,同时又有自己独特的属性和行为。例如,Vue实例有$mount
方法用于手动挂载实例到DOM,而VueComponent
实例通常不需要直接调用这个方法,它会在父组件注册和使用时,由Vue框架自动处理挂载过程。
4.3 通过vc可以访问Vue原型对象上的属性
通过vc可以访问Vue原型对象上的属性:
为什么要这么设计?代码复用。Vue原型对象上有很多方法,例如:$mount()
,对于组件VueComponent
来说就不需要再额外提供了,直接使用vc调用$mount()
,代码得到了复用。
Vue框架是如何实现以上机制的呢?
VueComponent.prototype.__proto__ = Vue.prototype
1、回顾原型对象
<script>// prototype __proto__// 构造函数(函数本身又是一种类型,代表Vip类型)function Vip() {}// Vip类型/Vip构造函数,有一个 prototype 属性。// 这个prototype属性可以称为:显式的原型属性。// 通过这个显式的原型属性可以获取:原型对象// 获取Vip的原型对象let x = Vip.prototype;// 通过Vip可以创建实例let a = new Vip();// 对于实例来说,都有一个隐式的原型属性: __proto__// 注意:显式的(建议程序员使用的)。隐式的(不建议程序员使用的。)// 这种方式也可以获取到Vip的原型对象let y = a.__proto__;// 原型对象只有一个,其实原型对象都是共享的。console.log(x === y); // true// 作用:// 在给“Vip的原型对象”扩展属性Vip.prototype.counter = 1000;// 通过a实例可以访问这个扩展的counter属性吗?可以访问。为什么?原理是啥?// 访问原理:首先去a实例上找counter属性,如果a实例上没有counter属性的话,//会沿着__proto__这个原型对象去找。// 下面代码看起来表面上是a上有一个counter属性,实际上不是a实例上的属性,是a实例对应的原型对象上的属性counter。console.log(a.counter);//console.log(a.__proto__.counter)
</script>
在JavaScript中,原型链是实现对象属性和方法继承的重要机制。通过将VueComponent.prototype.__proto__
设置为Vue.prototype
,VueComponent
实例就可以访问Vue原型对象上的属性和方法,实现了代码的复用。例如,如果在Vue原型对象上定义了一个全局的工具方法,那么所有的VueComponent
实例都可以直接调用这个方法,而无需在每个组件中重复定义。
2、底层实现
VueComponent.prototype.__proto__ = Vue.prototype
<body><div id="app"><h1>{{msg}}</h1><user></user></div><script>// 创建组件const user = Vue.extend({template: `<div><h1>user组件</h1></div>`,mounted() {// this是VueComponent实例// user是什么呢????是一个全新的构造函数 VueComponent构造函数。// 为什么要这样设计?为了代码复用。// 底层实现原理:// VueComponent.prototype.__proto__ = Vue.prototypeconsole.log("vc.counter", this.counter);},});// vmconst vm = new Vue({el: "#app",data: {msg: "vm与vc",},components: {user,},mounted() {// this是Vue实例console.log("vm", this);},});// 这个不是给Vue扩展counter属性。// 这个是给“Vue的原型对象”扩展一个counter属性。Vue.prototype.counter = 1000;console.log("vm.counter", vm.counter);// 本质上是这样的:console.log("vm.counter", vm.__proto__.counter);console.log("user.prototype.__proto__ === Vue.prototype", user.prototype.__proto__ === Vue.prototype);</script>
</body>
在上述代码中,通过将counter
属性添加到Vue原型对象上,VueComponent
实例(user
组件)和Vue实例(vm
)都可以访问到这个属性。通过console.log("user.prototype.__proto__ === Vue.prototype", user.prototype.__proto__ === Vue.prototype);
可以验证VueComponent
原型对象与Vue原型对象的关联关系,这就是实现VueComponent
实例能够访问Vue原型对象属性和方法的底层机制。