组件 (Component)是 Vue.js 最强大的功能之一,组件是一个自定义元素或称为一个模块,包括所需的模板(HTML)、逻辑(JavaScript)和样式(CSS)。
组件化开发的特点:
标准
分治
重用
组合
组件也是有 全局(component) 与 局部(components) 之分。
在使用组件时需要注意以下几点:
data: function(){
return {
msg: '你好世界'
}
}
组件模板 template
<div>
<ul>
<li>li>
ul>
<ul>
<li>li>
ul>
div>
<p>p>
<p>p>
组件名称命名方式
大驼峰式组件名不能在HTML模板中直接使用,如果需要在HTML模板中使用,需要将其进行特定规则转化:
首字母从大写转为小写
后续每遇到大写字母都要转化成小写并且在转化后的小写字母前加 -
例如, WoDeZuJian 这个大驼峰组件名在HTML中使用的时候需要写成 wo-de-zu-jian
全局组件注册形式如下:
// 声明全局组件
Vue.component(componentName,{
data: '组件数据',
template: '组件模版内容'
})
上述示例中, component() 的第一个参数是 组件名 (实则可以看作是HTML标签名称),第二个参数是一个对象形式的选项,里面存放组件的声明信息。全局组件注册后,任何Vue实例都可以使用。
例如,有以下代码:
// 声明一个全局的HelloWorld组件
Vue.component('HelloWorld', {
data: function(){
return {
msg: 'HelloWorld'
}
},
template: '{{msg}}'
});
实例:
<body>
组件的最大的优势:可以复用
<div id="app">
<btn-counter>btn-counter>
<btn-counter>btn-counter>
div>
<script src="../js/vue.js">script>
<script type="text/javascript">
// 创建新的全局组件
// 全局注册的组件可以用在其被注册之后的任何 (通过 new Vue) 新创建的Vue根实例,也包括其组件树中的所有子组件的模板中。
Vue.component("btn-counter", {
data: function () { //组件中的数据,组件的data必须是一个函数
return {
count: 0
}
},
//组件的模板
template: ``
})
var vm = new Vue({
el: "#app",
data: {
msg: "hello"
}
})
//注意当点击按钮时,每个组件都会各自独立维护它的 count。
//因为你每用一次组件,就会有一个它的新实例被创建。
script>
body>
局部组件定义后只能在当前注册它的Vue实例中使用,其是通过某个 Vue 实例/组件的实例选项components 注册。
例如,有以下代码:
var Child = {
template: 'A custom component!'
}
new Vue({
components: {
// 将只在父组件模板中可用
'my-component': Child
}
})
<body>
<div id="app">
<component-demo>component-demo>
div>
body>
<script src="../js/vue.js">script>
<script>
new Vue({
el: '#app',
components: {
'component-demo': {
template: '局部注册组件{{ num }}
',
data () {
return {
num: 500
}
},
computed: {},
watch: {},
mounted () {}
}
}
})
script>
在HTML模板中,组件以一个自定义标签的形式存在,起到占位符的功能。通过Vue.js的声明式渲染后,占位符将会被替换为实际的内容,下面是一个最简单的模块示例:
<div id="app">
<my-component>my-component>
div>
如前面介绍组件时所说,组件有 分治 的特点,每个组件之间具有一定的独立性,但是在实际工作中使用组件的时候有互相之间传递数据的需求,此时就得考虑如何进行 组件间传值 的问题了。
简单父传子:
<div id="app">
<ball value="足球" :test="num1">ball>
<ball value="篮球" :test="num2">ball>
<ball value="乒乓球" :test="num3">ball>
div>
<script type="text/javascript" src="../js/vue.js">script>
<script type="text/javascript">
//这个代码会报错
//因为组件只允许有一个根节点
var ball = {
//父组件传参给子组件时,使用 props接收
props: ['value', 'test'],//接收的父组件的传参
template: `我喜欢:{{value}}我进了 {{test}}个球`
};
var vm = new Vue({
el: "#app",
data: {
num1: 10,
num2: 20,
num3: 50
},
components: {
ball
}
})
script>
父组件以属性的形式绑定值到子组件身上
子组件通过使用属性props接收
props是单向绑定的(只读属性):当父组件的属性变化时,将传导给子组件,但是反过来不会
props属性支持两种常见的写法形式
数组
优点:书写简单
缺点:不能设置默认值、数据类型
对象
缺点:写法复杂
单向数据流实例:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>单向数据流title>
head>
<body>
<div id="app">
<child :name="user">child>
div>
<script src="./js/vue.js">script>
<script>
//1.定义子组件
//vue的组件传参是单向的,从父组件传过来的值,props接收后不能在子组件中进行修改
var child = {
props: ["name"],
template: `子组件, 从父组件传过来的name是:{{name}}
`,
methods: {
changeName() {
this.name = "laura";
}
}
}
/************************/
var app = new Vue({
el: "#app",
data: {
user: 'yyyy'
},
//2.注册
components: {
child
}
})
script>
body>
html>
使用computed来处理单向数据流:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>使用 计算属性解决问题title>
head>
<body>
<div id="app">
<child :name="user">child>
div>
<script src="./js/vue.js">script>
<script>
//1.定义子组件
//vue的组件传参是单向的,从父组件传过来的值,props接收后不能在子组件中进行修改
//将从父组件传过来的值重新赋值成组件本地变量
var child = {
props: ["name"],
data: function () {
return {
childname: this.name
}
},
template: `子组件, 从父组件传过来的name是:{{name}}
将从父组件传过来的值转大写{{changeUser}}
修改后的 值:{{childname}}
`,
computed: {
changeUser: function () {
return this.name.toUpperCase();
}
},
methods: {
changeN() {
this.childname = 'new name';
}
}
}
/************************/
var app = new Vue({
el: "#app",
data: {
user: 'yyyy'
},
//2.注册
components: {
child
}
})
script>
body>
html>
props数组、对象的写法:
<div id="app">
<child :propb="num" propc="test" propf="success">child>
div>
<script src="./js/vue.js">script>
<script>
var child = {
props: {
//可以验证传过来的参数类型
propb: [String, Number],
//****
propc: {
type: String,
required: true //参数是必填项
},
//****
propd: {
type: Number,
default: 8000 //参数的默认值
},
//默认值可以返回函数
prope: {
type: Object,
default: function () {
return { msg: 'hello' }
}
},
//自定义验证
propf: {
validator: function (value) {
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
},
template: `我是子组件
propb:{{propb}}
propc:{{propc}}
propd:{{propd}}
prope:{{prope}}
propf:{{propf}}
`
}
var app = new Vue({
el: "#app",
data: {
type: true,
num: 100
},
components: {
child
}
})
script>
子组件模版内容中用 $emit() 定义 自定义事件 , $emit() 方法有2个参数
第一个参数为自定义的事件名称
第二个参数为需要传递的数据(可选)
父组件模板内容中的子组件占位标签上用v-on(或@)绑定子组件定义的自定义事件名,监听子组件的事件,实现通信
子传父实例:
<div id="app">
父组件的msg:{{msg}}
<child :msg="msg" @myclick="parentFun">child>
div>
<script src="./js/vue.js">script>
<script>
//子组件
var child = {
props: ["msg"],
template: `子组件
从父组件传过来的参数是:{{msg}}
`,
methods: {
change() {
//第一步:使用 $emit 主动触发一个自定义事件
//参数:1.自定义的事件的名称,2.要传给父组件的参数
this.$emit("myclick", "newvalue");
}
}
}
/*****************/
var app = new Vue({
el: "#app",
data: {
msg: "hello"
},
components: {
child
},
methods: {
//第三步
parentFun: function (value) {
console.log("从子组件传过来的值:", value);
this.msg = value;
}
}
})
script>
自定义组件上使用 v-model:
<div id="app">
<input type="text" v-model="message" />{{message}}
<child v-model="msg">child>{{msg}}
div>
<script src="./js/vue.js">script>
<script>
var child = {
props: ['value'],
template: `
子组件
`
}
var app = new Vue({
el: "#app",
data: {
msg: "hello",
message: "hi"
},
components: {
child
}
})
script>
习题
要求:1.点击父组件按钮显示子组件,点击子组件隐藏本身,
2.使用.native 给子组件绑定原生事件
<div id="app">
<button @click="changeType">点击按钮显示子组件button>
<child v-show="type" @childclick="changeTT" @mouseover.native="test">child>
div>
<script src="./js/vue.js">script>
<script>
var child = {
template: `
我是子组件,我在蓝色的海洋里
`,
methods: {
childFun() {
//需要点击按钮改变父组件的type状态值
this.$emit("childclick", false);
}
}
}
var app = new Vue({
el: "#app",
data: {
type: false
},
components: {
child
},
methods: {
changeType() {
this.type = true;
},
// 接收子组件传过来的参数
changeTT(value) {
this.type = value;
},
test() {
console.log("给子组件绑定的原生事件触发了");
}
}
})
script>
.sync
在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件都没有明显的变更来源。
这也是为什么我们推荐以 update:myPropName 的模式触发事件取而代之。举个例子,在一个包含 title prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图:
this.$emit(‘update:title’, newTitle)
然后父组件可以监听那个事件并根据需要更新一个本地的数据 property。例如:
v-on:update:title=“doc.title = $event”
为了方便起见,我们为这种模式提供一个缩写,即 .sync 修饰符:
<div id="app">
<button @click="changeType">点击按钮显示子组件button>
<child v-show="isshow" :is-show.sync="isshow">child>
div>
<script src="./js/vue.js">script>
<script>
var child = {
template: `
我是子组件,我在蓝色的海洋里
`,
methods: {
childFun() {
//需要点击按钮改变父组件的type状态值
this.$emit("update:is-show", false);
}
}
}
var app = new Vue({
el: "#app",
data: {
isshow: false
},
components: {
child
},
methods: {
changeType() {
this.isshow = true;
},
// 接收子组件传过来的参数
// changeTT(value) {
// this.type = value;
// }
}
})
script>
EventBus又被称之为中央事件总线
在Vue中通过单独的 事件中心 来管理非 父子关系 组件(兄弟)间的通信:
公众号千千万,都得先关注公众号,一旦发送消息,就可以收到消息 - 专注交流一百年
核心步骤
const eventBus = new Vue()
eventBus.$emit('自定义事件名',传递的数据)
eventBus.$on('自定义事件名'[,callback])
eventBus.$off('自定义事件名')
先建立事件中心 const bus = new Vue()
在需要接受数据的地方先监听自定义事件以及接受数据的回调函数bus.$on('my-event', (data) => {})
在需要传递数据的地方提交 自定义事件以及参数 bus.$emit('my-event', params)
<div id="app">
<mycontent>mycontent>
<myfooter>myfooter>
div>
<template id="footer">
<ul class="nav">
<li @click="goContent('home')">首页li>
<li @click="goContent('list')">分类li>
<li @click="goContent('cart')">购物车li>
<li @click="goContent('me')">我的li>
ul>
template>
<template id="content">
<div class="con">我是内容页,显示的数据是:{{text}}div>
template>
<script src="./js/vue.js">script>
<script>
//兄弟节点间传参 eventbus
//1.创建bus
var bus = new Vue();
//子组件
//底部导航组件
var myfooter = {
template: '#footer',
methods: {
goContent(type) {
//2.发送数据
//参数:1.自定义事件名;2.要传递的参数
bus.$emit('myevent', type);
}
}
}
//内容显示组件
var mycontent = {
data() {
return {
text: ""
}
},
template: '#content',
//挂载后的钩子
mounted() {
//3. 接收数据
bus.$on('myevent', (data) => {
console.log(data);
this.text = data;
});
}
}
/**************/
var app = new Vue({
el: "#app",
components: {
myfooter,
mycontent
}
})
script>
ref 属性被用来给元素或子组件注册引用信息,引用信息将会注册在父组件的 $refs 对象上。
如果在普通的 DOM 元素上使用 ref 属性,则引用指向的就是 DOM 元素;
如果 ref 属性用在子组件上,引用就指向子组件实例。
ref 放在标签上,拿到的是原生节点。
ref 放在组件上 拿到的是组件实例
原理:在父组件中通过 ref 属性(会被注册到父组件的 $refs 对象上)拿到组件/DOM对象,从而得到组件/DOM中的所有的信息,也包括值
<div id="app">
<div ref="oDiv">父组件的div{{message}}div>
<child ref="child">child>
div>
<script src="./js/vue.js">script>
<script>
//refs 使用 ,是在 js原生组件或 自定义组件上写 ref 属性
//调用时,需要写成 this.$refs.ref的属性名.*****
//尽量不使用refs,因为 他没有使用虚拟dom ,性能稍差
//子组件
var child = {
data() {
return {
msg: 'hello'
}
},
template: `我是子组件`,
methods: {
test() {
console.log('子组件的函数被触发了');
}
}
}
var app = new Vue({
el: "#app",
data: {
message: ""
},
components: {
child
},
mounted() {
console.log(this);
console.log(this.$refs.oDiv);
//普通的div的使用refs修改其属性
this.$refs.oDiv.style.color = "green";
//使用 refs获取子节点的属性
console.log(this.$refs.child.msg);
this.message = this.$refs.child.msg;
this.$refs.child.test();
}
})
script>
注意:
ref 属性这种获取子元素/组件的方式虽然写法简单,容易上手,但是其由于权限过于开放,不推荐使用,有安全问题。(不仅可以获取值,还可以获取其他所有的元素/组件的数据,甚至可以修改这些数据。)
$nextTick
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
<div id="app">
<input type="text" v-show="isShow" ref="myinput" />
div>
<script src="./js/vue.js">script>
<script>
var app = new Vue({
el: "#app",
data: {
isShow: false
},
mounted: function () {
this.isShow = true;
//视图还没更新完,就获取到了dom节点的信息
// this.$refs.myinput.focus();
//这个问题可以使用 nextTick来解决
//nextTick 有一个参数 ,是回调函数,作用是,等待 视图更新后,在执行 回调函数中的代码
this.$nextTick(() => {
this.$refs.myinput.focus();
})
}
})
script>
可以用来从一个子组件访问父组件的实例,
在绝大多数情况下,触达父级组件会使得你的应用更难调试和理解,尤其是当你变更了父级组件的数据的时候。当我们稍后回看那个组件的时候,很难找出那个变更是从哪里发起的。
<div id="app">
<child>child>
div>
<script src="./js/vue.js">script>
<script>
//$root 可以获取到根节点的所有属性
//子组件
var child = {
data() {
return {
mm: ""
}
},
template: `子组件{{mm}}`,
mounted() {
this.$root.test();
this.mm = this.$root.msg;
}
}
var app = new Vue({
el: "#app",
data: {
msg: "hello"
},
methods: {
test() {
console.log("根节点的函数");
}
},
computed: {
bar() {
console.log("根节点的计算属性");
}
},
components: {
child
}
})
script>
可以通过$parent直接获取到父组件的实例,可以通过 $children直接获取到子组件的实例
如果需要在组件的结构中访问父组件的属性和方法,不需要添加this,但是也可以添加
通过 provide + inject 完成传值
这对选项是一起使用的。以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
provide:是一个对象,或者是一个返回对象的函数。里面包含要给子孙后代的东西,也就是属性和属性值。
inject:一个字符串数组,或者是一个对象。属性值可以是一个对象,包含from和default默认值。
from表示在可用的注入内容中搜索用的 key,default当然就是默认值。
provide
和inject
主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。
传参方式总结,以下几种方式都可以用来组件间传参:
通过分支条件判断实现选项卡切换
<div id="app">
<home v-if="type == 'home'">home>
<list v-else-if="type=='list'">list>
<cart v-else-if="type=='cart'">cart>
<me v-else>me>
<ul>
<li @click="goCon('home')">首页li>
<li @click="goCon('list')">列表li>
<li @click="goCon('cart')">购物车li>
<li @click="goCon('me')">我的li>
ul>
div>
<script src="./js/vue.js">script>
<script>
var home = {
template: `首页`
}
var list = {
template: `列表`
}
var cart = {
template: `购物车`
}
var me = {
template: `我的`
}
var app = new Vue({
el: "#app",
data: {
type: 'home'
},
methods: {
goCon(type) {
this.type = type;
}
},
components: {
home,
list,
cart,
me
}
})
script>
通过使用保留的 元素,动态地绑定到它的 is 特性,我们让多个组件可以使用同一个挂载点,并动态切换。
<div id="app">
<component :is="type">component>
<ul>
<li @click="goCon('home')">首页li>
<li @click="goCon('list')">列表li>
<li @click="goCon('cart')">购物车li>
<li @click="goCon('me')">我的li>
ul>
div>
<script src="./js/vue.js">script>
<script>
var home = {
template: `首页`
}
var list = {
template: `列表`
}
var cart = {
template: `购物车`
}
var me = {
template: `我的`
}
var app = new Vue({
el: "#app",
data: {
type: 'home'
},
methods: {
goCon(type) {
this.type = type;
}
},
components: {
home,
list,
cart,
me
}
})
script>
思考:如果每个组件中都有一个输入框,点击切换时输入不同的内容,然后再切换,查看效果
keep-alive的作用:
keep-alive 可以将已经切换出去的非活跃组件保留在内存中。如果把切换出去的组件保留在内存中,可以保留它的状态,避免重新渲染。
思考:使用keep-alive 看似保留了所有的状态,但是如果某一个组件不要保留状态呢
<keep-alive include="list,cart">
<component :is="type">component>
keep-alive>
//路由中可以配置组件缓存
<template>
<div id="app">
<keep-alive >
<router-view v-if="$route.meta.keepAlive">router-view> //放需要缓存的组件
keep-alive>
<router-view v-if="!$route.meta.keepAlive">router-view> //放不需要缓存的组件
div>
template>
//路由配置文件router.js文件中,给需要缓存的路由加上meta属性,并设置值。
{
path: '/usermanage',
name: 'usermanage',
meta: {
keepAlive: true, //该字段表示该页面需要缓存
},
component: resolve => require(['@/views/userManage/userManage'], resolve) // 路由懒加载
},
使用keep-alive之后会触发什么钩子函数
activated() {
console.log("keep-alive 组件进入时执行的钩子");
},
deactivated() {
console.log("keep-alive组件离开时执行的钩子");
}
案例:使用动态组件实现简易的步骤向导效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0UjiT9lu-1617193637113)(img/33.png)]
组件的最大特性就是 重用 ,而用好插槽能大大提高组件的可重用能力。
**插槽的作用:**父组件向子组件传递内容。
通俗的来讲,插槽无非就是在 子组件 中挖个坑,坑里面放什么东西由 父组件 决定。
插槽类型有:
单个(匿名)插槽
具名插槽
作用域插槽
匿名插槽一般就是使用单个插槽
注意:子组件的 slot 标签中允许书写内容,当父组件不往子组件传递内容时, slot 中的内容才会被展示出来。
slot 元素可以用一个特殊的特性 name 来进一步配置如何分发内容。多个插槽可以有不同的名字,具名插槽将匹配内容片段中有对应 slot 特性的元素。
上中下 形式网页布局示例代码
具名插槽存在的意义就是为了解决在单个页面中同时使用多个插槽。
**应用场景:**父组件对子组件的内容进行加工处理
作用域插槽是一种特殊类型的插槽,作用域插槽会绑定了一套数据,父组件可以拿这些数据来用,于是,情况就变成了这样:样式父组件说了算,但父组件中内容可以显示子组件插槽绑定的数据。
插槽实例,模拟element-ui封装弹窗ui组件:
插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:
Vue.prototype
上实现。通过全局方法 Vue.use()
使用插件。它需要在你调用 new Vue()
启动应用之前完成:
// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)
new Vue({
// ...组件选项
})
也可以传入一个可选的选项对象:
Vue.use(MyPlugin, { someOption: true })
Vue.use
会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件。
Vue.js 官方提供的一些插件 (例如 vue-router
) 在检测到 Vue
是可访问的全局变量时会自动调用 Vue.use()
。然而在像 CommonJS 这样的模块环境中,你应该始终显式地调用 Vue.use()
:
// 用 Browserify 或 webpack 提供的 CommonJS 模块环境时
var Vue = require('vue')
var VueRouter = require('vue-router')
// 不要忘了调用此方法
Vue.use(VueRouter)
Vue.js 的插件应该暴露一个 install
方法。这个方法的第一个参数是 Vue
构造器,第二个参数是一个可选的选项对象:
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})
// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
实例:
Vue 提供了 transition
的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡
v-if
)v-show
)实例:
实例: