目录
目标
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生成图~




















