02-Vue2组件&指令

image-20200723170734421

参考资料:

1. 组件

组件,扩展HTML元素,封装可重用的代码(让组件可以和html标签一样去使用)。vue最强大的功能之一

image-20251218180540448

1.1 创建组件

全局组件:Vue.component(tagName, options)

  • 起名字:①纯小写 ②js驼峰时,html标签使用-连接

  • template 属性需要包含一个根节点

  • 所有实例都能使用

1
2
3
4
5
6
7
8
9
10
11
12
<div id="app">
<navbar></navbar>
</div>
<script>
Vue.component('navbar', { // 全局组件 navbar
template: '<h1>自定义组件!</h1>',
methods: { handleXxx(){} ... }
})
new Vue({
el: '#app'
})
</script>

局部组件:new Vue({ el:..., components: {xxx:Child} })

  • 如果写在 Vue.component 中,就属于他自己的子组件,只能在自己的内部使用
  • 只能在当前初始化实例中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div id="app">
<navbar></navbar>
</div>
<script>
var Child = {
template: '<h1>自定义组件!</h1>'
}
new Vue({
el: '#app',
components: {
'navbar': Child // <navbar> 将只在当前实例可用
}
})
</script>

组件创建命名参考,如果使用 - 短杠分隔,那么就可以使用驼峰方式初始化组件。eg:

1
2
3
4
5
<film-detail></film-detail>

Vue.component("filmDetail", {
template: `<div></div>`
})

1.2 父子组件通信

1.2.1 父传子

  1. 子组件标签上通过:自定义属性绑定父组件的属性值
  2. 子组件的props属性通过该自定义属性接受父组件的属性值
    • props 校验类型 type 可以是下面原生构造器:
      • String
      • Number
      • Boolean
      • Array
      • Object
      • Date
      • Function
      • Symbol
    • props 是否能修改?父组件传给你的属性,只有父组件可以重新传,但不允许子组件随意修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<div id="box">
<div style="background: yellow;">根组件标题</div>
<!-- 变量:myname,myright,其中myright需要指定为js领域,才能识别为bool值 -->
<!-- 父传子 parent,需要 : 进行绑定,然后子组件进行接受该属性值 -->
<navbar myname="电影" :myright="false" :my_parent="parent"></navbar>
<navbar myname="影院" :myright="true" :my_parent="parent"></navbar>
</div>
<script>
// 子组件 navbar
Vue.component("navbar", {
//props: ["myname", "myright"], // 接受myname属性,使用 this.myname
// props: {
// myname: String,
// myright: Boolean
// }, // 接受myname属性,属性类型验证
props: {
myname: {
type: String,
default: ""
},
myright: {
type: Boolean,
default: true
},
my_parent: { // 子组件 navbar 接受父组件的属性值
type: String,
default: ""
}
}, // 接受myname属性,属性:类型验证、默认值
template: `<div>
<button>left</button>
<span>{{myname}}-{{my_parent}}</span>
<button v-show="myright">Q</button>
</div>`
})
// 父组件 box
new Vue({
el: "#box",
data: {
parent: "11111111111"
}
}) // 创建根组件
</script>

效果:

image-20251218193312941

1.2.2 子传父

  1. 子组件1标签上通过@自定义事件绑定父组件的事件方法
  2. 子组件1的 methods 点击事件中操作子组件自身的 this.$emit("自定义事件", 参数) 触发执行绑定父组件的事件方法
    • this.$emit(eventName, args...) 该方法为固定写法,且支持传参(也可以不传)
  3. 子组件2因为绑定了对应的变量,如 v-show,所以会跟随更新渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<div id="box">
<!-- 1. @父组件自定义事件(点击子组件navbar 控制显示或隐藏子组件sidebar) -->
<navbar @my_event="handleEvent"></navbar>
<sidebar v-show="isShow"></sidebar>
</div>
<script>
// 子组件 navbar
Vue.component("navbar", {
template: `
<div>
<button @click="handleClick()">点击</button>-导航栏
</div>`,
methods: {
handleClick() {
console.log("子传父")
// 2. $emit 触发/分发,子组件告诉父组件,触发父组件的事件 handleEvent
this.$emit("my_event", 100) // 支持传参
}
}
})
// 子组件 sidebar
Vue.component("sidebar", {
template: `
<div>
<ul>
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
<li>555</li>
</ul>
</div>`
})
// 父组件 box
new Vue({
el: "#box",
data: {
isShow: true
},
methods: {
handleEvent(arg) {
console.log("父组件定义的事件:", arg) // 父组件定义的事件: 100
this.isShow = !this.isShow
}
}
}) // 创建根组件
</script>

效果:类似抽屉效果

chrome-capture-2025-12-18

1.2.3 子传子(兄弟通信-中间人模式)

父组件,称之为 中间人。

  • 组合子传父、父传子的两个通信方式,即 子1传父、父传子2 来实现兄弟通信

示例:(列表与详情是两个同级的子组件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./lib/vue.js"></script>
<style>
.item{
overflow: hidden;
padding: 5px;
width: 300px;
border: 1px solid gray;
}
.item img{
float: left;
width: 100px;
}
.filmInfo{
max-width: 300px;
min-height: 200px;
position: fixed; /* 固定定位 */
right: 0;
top: 50px;
background: yellow;
}
</style>
</head>
<body>
<div id="box">
<button @click="handleClick">ajax</button>
<film-item v-for="item in datalist" :key="item.filmId" :item="item" @event="handleEvent"></film-item>
<!-- :detail-data 接收值时用 detailData, 渲染时用 {{detailData}} -->
<film-detail :detail-data="detailData"></film-detail>
</div>
<script>
Vue.component("filmItem", {
props: {
item: Object,
default: {}
},
template: `
<div class="item">
<img :src="item.poster">
{{item.name}}
<div>
<button @click="handleClick">详情</button>
</div>
</div>
`,
methods: {
handleClick() {
console.log(this.item.synopsis)
this.$emit("event", this.item.synopsis)
}
}
})

Vue.component("filmDetail", {
props: {
// 标签中定义属性为 :detail-data,接收时可以用驼峰 detailData
// 如果用下划线,则没有这个问题,都可以是同一个值 detail_data
detailData: {
type: String,
default: "没有简介"
}
},
template: `
<div class="filmInfo">
{{detailData}}
</div>
`
})

new Vue({
el: "#box",
data: {
datalist: [],
detailData: ""
},
methods: {
handleClick() {
fetch("./json/maizuo.json")
.then(res => res.json())
.then(res => {
console.log(res.data.films)
this.datalist = res.data.films
})
},
handleEvent(data) {
console.log("父组件自定义事件", data)
this.detailData = data
}
}
})
</script>
</body>
</html>

效果:

chrome-capture-2025-12-19

1.2.4 中央事件总线 bus

组件之间通信,关系变得复杂时,方案有二:

  • bus 中央事件总线 var bus = new Vue(),原理是 订阅发布模式
    • bus.$on() 监听事件,接收数据的组件中使用(记得定义接收数据的属性)
    • bus.$emit() 触发事件,发送数据的组件中使用
  • vuex 状态管理

作用:实现不同组件之间进行通信(非父子关系)。

原理:$bus就是vue原型上添加的一个公共的vue实例,用于存储、监听以及触发事件。

实现步骤:

  1. 注册事件总线
1
2
3
var bus = new Vue()
//或
Vue.prototype.bus = new Vue();
  1. 在需要发送信息的组件中触发事件
1
2
bus.$emit("eventname")        //无参传递,eventname表示事件名
bus.$emit("eventname",params) //带参传递,params表示传递的参数
  1. 在需要接收信息的组件中监听事件,需要写在 mounted() 函数中
1
2
3
4
5
6
7
//生命周期函数
mounted() {
//有参
bus.$on("eventname",(params)=>{...});
//无参
bus.$on('eventname', () => {...});
}
  1. 在接收信息的组件beforeDestroy事件中销毁接收事件
1
2
3
beforeDestroy() {
this.bus.$off('eventname');
}

示例:(改造列表与详情是两个组件的情况,模拟组件之间通信)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./lib/vue.js"></script>
<style>
.item{
overflow: hidden;
padding: 5px;
width: 300px;
border: 1px solid gray;
}
.item img{
float: left;
width: 100px;
}
.filmInfo{
max-width: 300px;
min-height: 200px;
position: fixed; /* 固定定位 */
right: 0;
top: 50px;
background: yellow;
}
</style>
</head>
<body>
<div id="box">
<button @click="handleClick">ajax</button>
<film-item v-for="item in datalist" :key="item.filmId" :item="item" @event="handleEvent"></film-item>
<!-- :detail-data 接收值时用 detailData, 渲染时用 {{detailData}} -->
<film-detail :detail-data="detailData"></film-detail>
</div>
<script>
// 事件总线 bus
var bus = new Vue()

Vue.component("filmItem", {
props: {
item: Object,
default: {}
},
template: `
<div class="item">
<img :src="item.poster">
{{item.name}}
<div>
<button @click="handleClick">详情</button>
</div>
</div>
`,
methods: {
handleClick() {
// console.log(this.item.synopsis)
bus.$emit("event", this.item.synopsis)
}
}
})

Vue.component("filmDetail", {
data() {
return {
info: ""
}
},
template: `
<div class="filmInfo">
{{info}}
</div>
`,
mounted() {
bus.$on("event", (params) => {
console.log("$on:", params)
this.info = params
})
}
})

new Vue({
el: "#box",
data: {
datalist: [],
detailData: ""
},
methods: {
handleClick() {
fetch("./json/maizuo.json")
.then(res => res.json())
.then(res => {
console.log(res.data.films)
this.datalist = res.data.films
})
},
handleEvent(data) {
console.log("父组件自定义事件", data)
this.detailData = data
}
}
})
</script>
</body>
</html>

效果一样。

扩展:v-once

v-once 有什么用?

渲染普通的HTML元素在VUE中是非常快速的,有时组件内包含了大量的静态内容,这种情况下可以在根元素上添加 v-once 属性,以确保这些内容只计算一次然后就被缓存起来了。

如:

1
2
3
Vue.component("terms-of-service", {
template: `<div v-once> ... </div>`
})

1.3 模版引用 ref

ref 绑定模版引用,它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。

this.$refs 获取引用对象

  • 绑定 dom 节点,拿到的就是 dom对象
  • 绑定 组件,拿到的就是 组件对象

注意:该引用如果被重新赋值,就打破了数据通信流程,导致数据流转混乱。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<div id="box">
<input type="text" ref="myText">
<input type="password" ref="myPassword">
<button @click="handleAdd">add</button>
<child ref="myChild"></child>
</div>
<script>
Vue.component("child", {
data() {
return {
myName: "jerry11111"
}
},
template: `<div>child-{{myName}}</div>`
})

new Vue({
el: "#box",
methods: {
handleAdd() {
// console.log(this.$refs.myText, this.$refs.myPassword)
console.log(this.$refs.myChild.myName)
// 该引用如果被重新赋值,就打破了数据通信流程,导致数据流转混乱
this.$refs.myChild.myName = "tom222222"
console.log(this.$refs.myChild.myName)
}
}
})
</script>

1.4 动态组件

<component is="xxx"></component> vue内置的标签,可以作为一个动态组件使用。

  • 如果需要保存该动态组件,如输入框中的已输入的值,则使用 <keep-alive></keep-alive>包裹
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<div id="box">
<button @click="which='first'">组件1</button>
<button @click="which='second'">组件2</button>
<button @click="which='third'">组件3</button>
<!-- 需要动态绑定 :is,否则which是字符串不是变量 -->
<component :is="which"></component>
</div>
<script>
Vue.component("first", {
template: `<div>111111</div>`
})
Vue.component("second", {
template: `<div>222222</div>`
})
Vue.component("third", {
template: `<div>333333</div>`
})

new Vue({
el: "#box",
data: {
which: "first"
}
})
</script>

1.5 插槽 slot

<slot></slot>v-slot:# 插槽,也叫内容分发。 扩展组件的能力,提高组件的复用性。会将对应的内容直接进行替换。

  • 单个插槽,就写标签即可 <slot></slot>
  • 具名插槽,给属性 name,即 <slot name="xxx"></slot>
  • 新版slot写法,将 v-slot:name 简写为 #name,该写法只能写在 <template> 标签上

内容分发:混合父组件的内容与子组件自己的模版。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="box">
<child>
<template v-slot:a>
<div>aaaaaaaaaaaaa</div>
</template>
<!-- 简写 # -->
<template #b>
<div>bbbbbbbbbbbbb</div>
</template>
</child>
</div>
<script>
Vue.component("child", {
template: `
<div>
<slot name="a"></slot>
<slot name="b"></slot>
</div>
`
})
new Vue({
el: "#box"
})
</script>

旧版插槽 slot:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="box">
<div>11111</div>
<div>22222</div>
<div slot="a">aaaaa</div>
<div slot="b">bbbbb</div>
</div>
<script>
Vue.component("child", {
template: `
<div>
<slot></slot>
<slot name="a"></slot>
<slot name="b"></slot>
</div>
`
})
new Vue({
el: "#box"
})
</script>

1.6 过渡动画

transition 标签包裹需要加动画的内容。参考资料:https://www.runoob.com/vue2/vue-transitions.html

  • <transition name="jerry" appear mode="in-out"> 内部默认只生效1个元素(可用div包裹为1个) </transition>

    • name=”jerry” 其中的 jerry是动画 class 名称的前缀,会被拆解匹配为
      • .jerry-enter-active {动画效果CSS样式}
      • .jerry-leave-active {动画效果CSS样式}
    • appear 属性是设置初始元素过渡动画,让页面刷新时初始动画也能生效
    • mode 可以设置为 in-out 或者 out-in,意为 先进后出动画,或者 先出后进动画
  • 当有相同标签名的元素切换时,通过 key 属性设置唯一值来标记它们,配合 v-if 和 v-else 在动画中切换内容

    • diff算法带来的效果:
      • 如果key一样就复用,如果key不一样就会删除并创建新的对比
      • 如果标签不一样,也会删除dom节点重新创建
  • <transition-group name="jerry"></transition-group> 用于多个列表的过渡,列表元素需要设置唯一的key值(内部还是dom的diff算法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./lib/vue.js"></script>
<style>
.jerry-enter-active {
animation: aaa 1s;
}

.jerry-leave-active {
animation: aaa 1s reverse;
}

@keyframes aaa {
0% {
opacity: 0;
transform: translateX(100px);
}

100% {
opacity: 1;
transform: translateX(0px);
}
}
</style>
</head>

<body>
<div id="box">
<button @click="isShow = !isShow">change</button>
<transition enter-active-class="jerry-enter-active" leave-active-class="jerry-leave-active">
<div v-show="isShow">hello, change</div>
</transition>

<!-- 简写的方式,只写前缀; 初始动画(刷新页面就出现) -->
<transition name="jerry" appear mode="out-in">
<div v-if="isShow" key="1">hello, jerry1</div>
<div v-else key="2">hello, jerry2</div>
</transition>
</div>
<script>
new Vue({
el: "#box",
data: {
isShow: true
}
})
</script>
</body>

</html>

效果:

chrome-capture-2025-12-19 (1)

案例:待办事项-列表过渡动画

1
2
3
4
5
6
7
8
<ul v-show="dataList.length">
<transition-group name="jerry">
<li v-for="(item, index) in dataList" :key="item">
{{item}}-{{index}}
<button @click="handleDel(index)">删除</button>
</li>
</transition-group>
</ul>

案例:抽屉效果-过渡动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./lib/vue.js"></script>
<style>
.jerry{
position: fixed;
right: 0px;
top: 0px;
}
/* 改变前缀 jerry 做多套动画,用于匹配 mode 不同的动画 */
.jerry-enter-active {
animation: aaa 1s;
}

.jerry-leave-active {
animation: aaa 1s reverse;
}

@keyframes aaa {
0% {
opacity: 0;
transform: translateX(100px);
}

100% {
opacity: 1;
transform: translateX(0px);
}
}
</style>
</head>

<body>
<div id="box">
<navbar>
<button @click="isShow = !isShow">点击</button>
</navbar>
<!-- 多套CSS,对应接收的 mode 的值,作为CSS的前缀 -->
<sidebar v-show="isShow" mode="jerry"></sidebar>
</div>
<script>
// 子组件 navbar
Vue.component("navbar", {
template: `
<div>
导航栏-<slot></slot>
</div>`,
methods: {
handleClick() {
console.log("子传父")
// 2. $emit 触发/分发,子组件告诉父组件,触发父组件的事件 handleEvent
this.$emit("my_event", 100) // 支持传参
}
}
})
// 子组件 sidebar
Vue.component("sidebar", {
props: {
mode: {
type: String,
default: ""
}
},
// mode 值使用时,需要指定 name为绑定的属性 :name
template: `
<transition :name=mode>
<div>
<ul :class=mode>
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
<li>555</li>
</ul>
</div>
</transition>
`
})
// 父组件 box
new Vue({
el: "#box",
data: {
isShow: false
},
methods: {
handleEvent(arg) {
console.log("父组件定义的事件:", arg) // 父组件定义的事件: 100
this.isShow = !this.isShow
}
}
}) // 创建根组件
</script>
</body>

</html>

1.7 生命周期

四个阶段,八个钩子函数。创建 和 挂载,即前4个生命周期一个组件只会走一次。

①创建:

  • beforeCreate() 【实例创建之前】数据获取不到,dom元素没有渲染

  • created()实例已创建】可以访问到数据,真实dom元素还没有渲染,可以进行相关初始化的事件绑定、发送请求操作

②挂载:

  • beforeMount() 【实例挂载之前】相关render函数首次被调用,但数据还没有被渲染,与created()函数用法一致

  • mounted()实例已挂载】:真实dom元素已经渲染完成,可以拿到真实dom节点

    • 很多依赖dom创建之后才能进行初始化工作的插件,如轮播插件
    • 订阅bus总线 bus.$on
    • 发 ajax(即打开页面就像看到的数据)

③更新:

  • beforeUpdate()实例数据更新之前】数据发生改变,但在dom被更新之前调用,可以获取dom被更新前的内容

  • updated() 【实例数据已更新】可以获取到diff算法对比后把虚拟dom更新为真实dom后的真实dom渲染了

④销毁:

  • beforeDestroy()-vue2 | beforeUnmount()-vue3【实例销毁之前】实例仍然完全可用,可以做善后处理,如清楚定时器、事件解绑等

  • destroyed-vue2 | unmounted()-vu3【实例已销毁】完全丧失功能

生命周期图示vue2:

Vue 实例生命周期

生命周期图示vue3:

img

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<div id="box">
{{myname}} - {{globalName}}
<button @click=" myname = 'tom' ">更新</button>
</div>
<script>
new Vue({
el: "#box",
data: {
myname: "jerry",
user: {}
},
beforeCreate() {
console.log("beforeCreate ->", this.myname)
},
created() {
// 一般做状态的初始化工作
console.log("created ->", this.myname)
this.myname = this.myname + "111111111"
this.globalName = "this可以直接访问的属性值"
this.user = localStorage.getItem("user") // this下面的属性
},
beforeMount() {
console.log("beforeMount ->", this.$el) //模版解析之前最后一次修改模版节点
},
mounted() {
console.log("mounted ->", this.$el) //真实数据已经渲染,可以做相关初始化工作,发ajax
setTimeout(() => {
this.datalist = ["aaa", "bbb", "ccc"]
console.log("长度:", document.getElementsByTagName("li").length) //0
}, 2000)
},
beforeUpdate() {
console.log("beforeUpdate ->", this.myname)
},
updated() {
console.log("updated ->", this.myname)
},
beforeUnmount() {
console.log("beforeUnmount ->")
window.onresize = null
},
unmounted() {
console.log("unmounted ->")
}
})
</script>

案例:封装轮播swiper组件-vue2&3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="../lib/swiper-bundle.min.css">
<script src="../lib/swiper-bundle.min.js"></script>
<script src="../lib/vue.js"></script>
<style>
.swiper {
width: 600px;
height: 300px;
}
</style>
</head>

<body>
<div id="box">
<!-- 方案1:加 :key 为数据的长度,异步中必须添加-官方要求,以确保轮播根据数组长度创建出来 -->
<!-- <swiper :key="datalist.length"> -->
<!-- 方案2:加 v-if 可以在数组长度不为 0 时才创建出来轮播组件,此时只会mounted一次【推荐】-->
<swiper v-if="datalist.length" :loop="true">
<swiper-item v-for="(item, index) in datalist" :key="item">
<img :src="item" />
</swiper-item>
</swiper>
</div>

<script>
Vue.component("swiperItem", {
template: `
<div class="swiper-slide">
<slot></slot>
</div>
`
})

Vue.component("swiper", {
props: {
/* 接收属性传值,并验证属性 */
loop: {
type: Boolean,
default: true
}
},
template: `
<div class="swiper">
<div class="swiper-wrapper">
<slot></slot>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
</div>
`,
mounted() {
console.log("mounted")
new Swiper(".swiper", {
loop: this.loop, // 循环模式选项
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 自动轮播
autoplay: {
delay: 2000,
disableOnInteraction: false,
},
})
},
destroyed() {
console.log("destroyed")
}
})

new Vue({
el: "#box",
data: {
datalist: []
},
mounted() {
setTimeout(() => {
this.datalist = ["https://static.maizuo.com/pc/v5/usr/movie/71207b4b172609411d1b2e429ea08961.jpg?x-oss-process=image/quality,Q_70", "https://static.maizuo.com/pc/v5/usr/movie/53443bf08ac8f08d23e3fe35959a3240.jpg?x-oss-process=image/quality,Q_70", "https://static.maizuo.com/pc/v5/usr/movie/a9ce1fce280bd093b0bfab88dd05317a.jpg?x-oss-process=image/quality,Q_70"]
}, 2000)
}
})
</script>
</body>

</html>

vue3写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="../lib/swiper-bundle.min.css">
<script src="../lib/swiper-bundle.min.js"></script>
<script src="../lib/vue.global.js"></script>
<style>
.swiper {
width: 600px;
height: 300px;
}
</style>
</head>

<body>
<div id="box">
<!-- 方案1:加 :key 为数据的长度,异步中必须添加-官方要求,以确保轮播根据数组长度创建出来 -->
<!-- <swiper :key="datalist.length"> -->
<!-- 方案2:加 v-if 可以在数组长度不为 0 时才创建出来轮播组件,此时只会mounted一次【推荐】-->
<swiper v-if="datalist.length" :loop="true">
<swiper-item v-for="(item, index) in datalist" :key="item">
<img :src="item" />
</swiper-item>
</swiper>
</div>

<script>
var obj = {
data() {
return {
datalist: []
}
},
mounted() {
setTimeout(() => {
this.datalist = ["https://static.maizuo.com/pc/v5/usr/movie/71207b4b172609411d1b2e429ea08961.jpg?x-oss-process=image/quality,Q_70", "https://static.maizuo.com/pc/v5/usr/movie/53443bf08ac8f08d23e3fe35959a3240.jpg?x-oss-process=image/quality,Q_70", "https://static.maizuo.com/pc/v5/usr/movie/a9ce1fce280bd093b0bfab88dd05317a.jpg?x-oss-process=image/quality,Q_70"]
}, 2000)
}
}

let app = Vue.createApp(obj)
app.component("swiperItem", {
template: `
<div class="swiper-slide">
<slot></slot>
</div>
`
})
app.component("swiper", {
props: {
/* 接收属性传值,并验证属性 */
loop: {
type: Boolean,
default: true
}
},
template: `
<div class="swiper">
<div class="swiper-wrapper">
<slot></slot>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
</div>
`,
mounted() {
console.log("mounted")
new Swiper(".swiper", {
loop: this.loop, // 循环模式选项
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 自动轮播
autoplay: {
delay: 2000,
disableOnInteraction: false,
},
})
},
destroyed() {
console.log("destroyed")
}
})
app.mount("#box")
</script>
</body>

</html>

2. 指令

2.1 自定义指令

参考资料:https://www.runoob.com/vue2/vue-custom-directive.html

Vue.directive()自定义指令,对普通DOM元素进行底层操作。(对底层DOM的一种封装)

实际应用:通过指令知道什么时候dom创建完成,从而进行依赖dom库的初始化工作。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<div id="box">
<div v-hello>1111111111111</div>
<div v-hello=" 'yellow' ">2222222222222</div>
<div v-hello="whichColor">3333333333333</div>
</div>
<script>
// 定义 hello,使用 v-hello
Vue.directive("hello", {
// 指令的生命周期函数 inserted 自动被调用,第一次插入到父节点中被触发
// - el 元素对象
// - binding 接收参数,binding.value 取值
inserted(el, binding) {
console.log("inserted", el)
// 通过el可以去操作原生的dom
el.style.background = "red"
el.style.background = binding.value
},
// 指令的生命周期函数 update:数据更新的时候会被调用
update(el, binding) {
console.log("update")
el.style.background = binding.value
}
})
var vm = new Vue({
el: "#box",
data: {
whichColor: "blue"
}
})
</script>

指令函数简写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="box">
<div v-hello>1111111111111</div>
<div v-hello=" 'yellow' ">2222222222222</div>
<div v-hello="whichColor">3333333333333</div>
</div>
<script>
// 定义 hello,使用 v-hello,函数简写
Vue.directive("hello", (el, binding) => {
console.log("创建或更新都会去执行", el)
el.style.background = "red"
el.style.background = binding.value
})
var vm = new Vue({
el: "#box",
data: {
whichColor: "blue"
}
})
</script>

2.2 指令应用

轮播改造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="../lib/swiper-bundle.min.css">
<script src="../lib/swiper-bundle.min.js"></script>
<script src="../lib/vue.js"></script>
<style>
.swiper {
width: 600px;
height: 300px;
}
</style>
</head>

<body>
<div id="box">
<header>导航</header>
<div class="swiper">
<div class="swiper-wrapper">
<!-- 绑一个自定义指令 v-swiper,传参为对象解构 -->
<div class="swiper-slide" v-for="(item, index) in datalist" :key="item"
v-swiper="{index: index, length: datalist.length}">
{{item}}
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>

<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
<footer>底部内容</footer>
</div>


<script>
// 自定义指令 v-swiper
Vue.directive("swiper", {
inserted(el, binding) {
console.log("inserted", el, binding.value)
// 如果最后一个节点插入到父节点中了,就可以 new Swiper 初始化了
let { index, length } = binding.value
if (binding.value = length) {
console.log("new Swiper")
new Swiper(".swiper", {
// direction: 'vertical', // 垂直切换选项
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 自动轮播
autoplay: {
delay: 2500,
disableOnInteraction: false,
},
})
}
}
})

// swiper初始化过早 - 需要在dom节点创建后再创建swiper
new Vue({
el: "#box",
data: {
datalist: []
},
mounted() {
setTimeout(() => {
this.datalist = ["aaa", "bbb", "ccc"]
//过早
}, 2000)
},
})
</script>
</body>

</html>

2.3 指令函数

  • bind: 只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个在绑定时执行一次的初始化动作。
  • inserted: 被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于 document 中)。
  • update: 被绑定元素所在的模板更新时调用,而不论绑定值是否变化。但是可能发生在其子Vnode更新之前。通过比较更新前后的绑定值,可以忽略不必要的模板更新(详细的钩子函数参数见下)。
  • componentUpdated: 指令所在组件的VNode及其子VNode全部更新后调用。
  • unbind: 只调用一次, 指令与元素解绑时调用。

2.4 $nextTick

指令轮播黑魔法:$nextTick 能解决一些问题,但没有什么复用性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<div id="box">
<header>导航</header>
<div class="swiper">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(item, index) in datalist" :key="item">
{{item}}
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>

<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>

<!-- 如果需要滚动条 -->
<!-- <div class="swiper-scrollbar"></div> -->
</div>
<footer>底部内容</footer>
</div>
<script>
// swiper初始化过早 - 需要在dom节点创建后再创建swiper
new Vue({
el: "#box",
data: {
datalist: []
},
mounted() {
setTimeout(() => {
this.datalist = ["aaa", "bbb", "ccc"]

// 黑魔法:能解决一些问题,但没有什么复用性
this.$nextTick(() => {
console.log("此处比updated执行的都晚,而且只执行一次")

new Swiper(".swiper", {
// direction: 'vertical', // 垂直切换选项
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 自动轮播
autoplay: {
delay: 2500,
disableOnInteraction: false,
},
})
})

}, 2000)
},
})
</script>

02-Vue2组件&指令
https://janycode.github.io/2018/05/22/04_大前端/04_Vue/02-Vue2组件&指令/
作者
Jerry(姜源)
发布于
2018年5月22日
许可协议