vue:组件的使用
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)
},
});
// vm
const 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.prototype
console.log("vc.counter", this.counter);
},
});
// vm
const 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原型对象属性和方法的底层机制。