-
Notifications
You must be signed in to change notification settings - Fork 139
/
VueLazyComponent.vue
146 lines (132 loc) · 3.62 KB
/
VueLazyComponent.vue
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
<template>
<transition-group :tag="tagName" name="lazy-component" style="position: relative;"
@before-enter="(el) => $emit('before-enter', el)"
@before-leave="(el) => $emit('before-leave', el)"
@after-enter="(el) => $emit('after-enter', el)"
@after-leave="(el) => $emit('after-leave', el)"
>
<div v-if="isInit" key="component">
<slot :loading="loading"></slot>
</div>
<div v-else-if="$slots.skeleton" key="skeleton">
<slot name="skeleton"></slot>
</div>
<div v-else key="loading">
</div>
</transition-group>
</template>
<script>
export default {
name: 'VueLazyComponent',
props: {
timeout: {
type: Number
},
tagName: {
type: String,
default: 'div'
},
viewport: {
type: typeof window !== 'undefined' ? window.HTMLElement : Object,
default: () => null
},
threshold: {
type: String,
default: '0px'
},
direction: {
type: String,
default: 'vertical'
},
maxWaitingTime: {
type: Number,
default: 50
}
},
data () {
return {
isInit: false,
timer: null,
io: null,
loading: false
}
},
created () {
// 如果指定timeout则无论可见与否都是在timeout之后初始化
if (this.timeout) {
this.timer = setTimeout(() => {
this.init()
}, this.timeout)
}
},
mounted () {
if (!this.timeout) {
// 根据滚动方向来构造视口外边距,用于提前加载
let rootMargin
switch (this.direction) {
case 'vertical':
rootMargin = `${this.threshold} 0px`
break
case 'horizontal':
rootMargin = `0px ${this.threshold}`
break
}
try {
// 观察视口与组件容器的交叉情况
this.io = new window.IntersectionObserver(this.intersectionHandler, {
rootMargin,
root: this.viewport,
threshold: [0, Number.MIN_VALUE, 0.01]
});
this.io.observe(this.$el);
} catch (e) {
this.init()
}
}
},
beforeDestroy () {
// 在组件销毁前取消观察
if (this.io) {
this.io.unobserve(this.$el)
}
},
methods: {
// 交叉情况变化处理函数
intersectionHandler (entries) {
if (
// 正在交叉
entries[0].isIntersecting ||
// 交叉率大于0
entries[0].intersectionRatio
) {
this.init()
this.io.unobserve(this.$el)
}
},
// 处理组件和骨架组件的切换
init () {
// 此时说明骨架组件即将被切换
this.$emit('beforeInit')
this.$emit('before-init')
// 此时可以准备加载懒加载组件的资源
this.loading = true
// 由于函数会在主线程中执行,加载懒加载组件非常耗时,容易卡顿
// 所以在requestAnimationFrame回调中延后执行
this.requestAnimationFrame(() => {
this.isInit = true
this.$emit('init')
})
},
requestAnimationFrame (callback) {
// 防止等待太久没有执行回调
// 设置最大等待时间
setTimeout(() => {
if (this.isInit) return
callback()
}, this.maxWaitingTime)
// 兼容不支持requestAnimationFrame 的浏览器
return (window.requestAnimationFrame || ((callback) => setTimeout(callback, 1000 / 60)))(callback)
}
}
}
</script>