目录
目标
keep alive
util/vue.js【vue里面常用的函数】
src/components/UKeepAlive.vue
无限加载列表优化的实现方案
src/util/throttle.js
src/components/UInfiniteList.vue
src/module/topic/views/UTopic.vue
献上一张ai生成图~
目标
- Keep Alive实践
- 长列表优化
keep alive
keep alive缓存组件
可以缓存router-view
// 整个路由做个keep-alive的缓存,减少dom直接生成的性能损耗
<keep-alive max="2">// max 缓存个数
<router-view></router-view>
</keep-alive>
自己实现一个keep-alive组件
util/vue.js【vue里面常用的函数】
// src/util/vue.js
// 已经定义过的值,非undefined,非null
function isDef(x) {
return x !== undefined && x !== null;
}
// 根据item【数组的一项值】删除此项
function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1);
}
}
}
// 获取opts实例上的名字
function getComponentName(opts) {
return opts && (opts.Ctor.options.name || opts.tag);
}
// children 是vue实例上的子节点,子数据
function getFirstComponentChild(children) {
if (Array.isArray(children)) { // 是否是数组
for (let i = 0; i < children.length; i++) {
const c = children[i];
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c; // 返回第一个定义的子组件数据
}
}
}
}
export { isDef, remove, getComponentName, getFirstComponentChild };
src/components/UKeepAlive.vue
// src/components/UKeepAlive.vue
<script>
import {
isDef,
remove,
getComponentName,
getFirstComponentChild
} from "../util/vue";
function pruneCacheEntry(cache, key, keys, current) {
// 当前key的cache
const cached = cache[key];
// 当前key的cache存在,如果当前current不存在或者当前tag不等于current.tag
// 他们的标签不一样
if (cached && (!current || cached.tag !== current.tag)) {
// 组件实例销毁方法
cached.componentInstance.$destroy();
}
// 清空当前key的缓存
cache[key] = null;
// 删除keys里的key
remove(keys, key);
}
export default {
name: "u-keep-alive",
abstract: true,
props: {
max: [Number, String]
},
created() {
// 对应的组件映射成key,放到keys里,通过key索引到cache里
this.keys = [];
// 创造一个缓存节点
// 对象原型就是null,不会包含object方法,提升性能
this.cache = Object.create(null);
},
// 渲染
render() {
// 获取u-keep-alive组件下的信息
const slot = this.$slots.default;
// 获取组件第一个子组件的vnode
const vnode = getFirstComponentChild(slot);
// 获得这个子组件信息
const componentOptions = vnode && vnode.componentOptions;
if (componentOptions) { // 存在,做一些缓存处理操作
const { cache, keys } = this; // 解构出created时候定义的this
// 定义一下key
const key =
vnode.key == null
// 如果key不存在,得生成个key,组件【`${id}::${tag}`】表示key
? `${componentOptions.Ctor.cid}::${componentOptions.tag || ""}`
: vnode.key;
// 如果刚刚这个key,在缓存里有了,
if (cache[key]) {
// 直接把缓存赋值给【当前组件实例】
vnode.componentInstance = cache[key].componentInstance;
// 删除,添加,更新一下,把key放在最后
remove(keys, key);
keys.push(key);
} else { // 不存在缓存,缓存一下
cache[key] = vnode;
keys.push(key);
// 如果设置了最大缓存数量,并且超出了
if (this.max && keys.length > parseInt(this.max)) {
// 把最早使用的清除keys[0]【移除最早的缓存cache】
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
// 开关标记keepAlive为true,并且vnode.componentInstance存在,就会直接渲染
vnode.data.keepAlive = true;
}
return vnode; // 渲染vnode
}
};
</script>
无限加载列表优化的实现方案
src/util/throttle.js
// 节流
export function throttle(fn, timeout = 10) {
let timer;
return function(...args) {
if (timer) {
timer = clearTimeout(timer);
}
timer = setTimeout(() => fn.apply(this, [...args]), timeout);
};
}
src/components/UInfiniteList.vue
// renderless方案
<template>
<div class="x-infinite" ref="container" :style="{padding: padding}"> // 上下滚动条位置填充占位
// 暴漏出去sliceItems
<slot :sliceItems="sliceItems"></slot>
</div>
</template>
<script>
import { throttle } from "../util/throttle"; // 节流函数
function getScrollTop() {
return document.documentElement.scrollTop || document.body.scrollTop;
}
export default {
props: {
items: {
required: true
},
itemHeight: { // 每条item高度
required: true,
type: Number
}
},
data() {
return {
buffer: 5,// 缓冲
scrollTop: 0,// 页面高度
viewportHeight: 0// 当前视口高度
};
},
computed: {
over() { // 滚动过的节点个数
return Math.max(//保证值大于0
Math.floor(this.scrollTop / this.itemHeight) - this.buffer,
0
);
},
down() { // 将要滚动到的节点个数
return Math.min(
Math.ceil(
(this.scrollTop + this.viewportHeight) / this.itemHeight + this.buffer
),
this.items.length
);
},
sliceItems() { // 中间的片段
return this.items.slice(this.over, this.down);
},
padding() { // 计算当前dom结构的padding值,上下滚动条位置填充占位
return `${this.over * this.itemHeight}px // padding-top
0
${Math.max( (this.items.length - this.down) * this.itemHeight, 0 )}px // padding-bottom
0`;
}
},
created() {
// 监听滚动事件
// 初始值设置
this.scrollTop = getScrollTop();
this.viewportHeight = window.innerHeight;
document.addEventListener("scroll", this.onScroll, {
passive: true // 提升流畅度,不用等待onScroll执行完毕
});
},
// 销毁事件监听
destroyed() {
document.removeEventListener("scroll", this.onScroll);
},
methods: {
onScroll: throttle(function() {
this.scrollTop = getScrollTop();
this.viewportHeight = window.innerHeight;
})
}
};
</script>
src/module/topic/views/UTopic.vue
<template>
<div>
<u-infinite-list :items="items" :item-height="80" #default="{ sliceItems }">
<u-list :items="sliceItems"></u-list>
</u-infinite-list>
自定义指令intersect,出现在屏幕中执行handler
<div class="x-bottom" v-intersect="{ handler: fetchNext }">
滚动到底部,加载下一页
</div>
</div>
</template>
<script>
import { createNamespacedHelpers } from "vuex";
import UList from "../components/UList.vue";
import UInfiniteList from "../../../components/UInfiniteList.vue";
// 便捷的映射topic命名空间的一个mapState方法
const { mapState, mapActions } = createNamespacedHelpers("topic");
export default {
name: "u-top",
props: ["type"],
components: {
UList,
UInfiniteList,
},
computed: {
...mapState({
items: (state) => state[state.activeType].items,
}),
},
// asyncData({ store, route }) {
// return store
// .dispatch("topic/FETCH_LIST_DATA", {
// type: route.name
// })
// .catch(e => console.error(e));
// },
watch: {
type(type) {
this.fetchData({ type });
},
},
mounted() {
this.fetchNext();
},
methods: {
...mapActions({
fetchData: "FETCH_LIST_DATA",
}),
fetchNext() {
const { type } = this;
this.fetchData({ type });
},
},
};
</script>
<style scoped>
.x-bottom {
width: 100%;
height: 1px;
}
</style>
献上一张ai生成图~