使用 key

对于通过循环生成的列表,应给每个列表项一个稳定且唯一的 key ,这有利于在列表变动时,尽量少的删除、新增、改动元素;

使用冻结对象

冻结的对象不会被响应化;

  1. App.vue

    <template>
      <div id="app">
        <button @click="loadNormalDatas">load normal datas</button>
        <button @click="loadFrozenDatas">load frozen datas</button>
        <h1>normal datas count: {{ normalDatas.length }}</h1>
        <h1>freeze datas count: {{ freezeDatas.length }}</h1>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          normalDatas: [],
          freezeDatas: [],
        };
      },
      methods: {
        loadNormalDatas() {
          this.normalDatas = this.getDatas();
          console.log('normalDatas', this.normalDatas);
        },
        loadFrozenDatas() {
          // 使用冻结对象
          this.freezeDatas = Object.freeze(this.getDatas());
          console.log('freezeDatas', this.freezeDatas);
        },
        getDatas() {
          const result = [];
          for (var i = 0; i < 1000000; i++) {
            result.push({
              id: i,
              name: `name${i}`,
              address: {
                city: `city${i}`,
                province: `province${i}`,
              },
            });
          }
          return result;
        },
      },
    };
    </script>
    
    <style>
    #app {
      text-align: center;
    }
    </style>
    
  2. 使用灯塔监控性能

使用函数式组件

参见函数式组件

  1. App.vue

    <template>
      <div id="app">
        <button @click="normalCount = 10000">生成10000个普通组件</button>
        <button @click="functionalCount = 10000">生成10000个函数组件</button>
    
        <div class="container">
          <div class="item">
            <NormalComp v-for="n in normalCount" :key="n" :count="n"></NormalComp>
          </div>
          <div class="item">
            <FunctionalComp v-for="n in functionalCount" :key="n" :count="n"></FunctionalComp>
          </div>
        </div>
      </div>
    </template>
    
    <script>
    import NormalComp from './components/NormalComp';
    import FunctionalComp from './components/FunctionalComp';
    export default {
      components: {
        NormalComp,
        FunctionalComp,
      },
      data() {
        return {
          functionalCount: 0,
          normalCount: 0,
        };
      },
      mounted() {
        window.vm = this;
      },
    };
    </script>
    
    <style>
    #app {
      text-align: center;
    }
    .container {
      width: 90%;
      display: flex;
      margin: 0 auto;
    }
    .item {
      padding: 30px;
      border: 1px solid #ccc;
      margin: 1em;
      flex: 1 1 auto;
    }
    </style>
    
  2. 普通组件

    <template>
      <h1>NormalComp: {{ count }}</h1>
    </template>
    
    <script>
    export default {
      props: {
        count: Number,
      },
    };
    </script>
    
    <style></style>
    
  3. 函数式组件

    <template functional>
      <h1>NormalComp: {{ props.count }}</h1>
    </template>
    
    <script>
    export default {
      functional: true,
      props: {
        count: Number,
      },
    };
    </script>
    
    <style></style>
    
  4. 灯塔测试函数式组件的性能

使用计算属性

如果模板中某个数据会使用多次,并且该数据是通过计算得到的,使用计算属性以缓存它们;

保持对象引用稳定

  1. 在绝大部分情况下,vue 触发 rerender 的时机是其依赖的数据发生 变化,若数据没有发生变化,哪怕给数据重新赋值了, vue 也是不会做出任何处理的;

  2. 下面是 vue 判断数据 没有变化 的源码:

    // value 为旧值, newVal 为新值
    if (newVal === value || (newVal !== newVal && value !== value)) {
      return
    }
    
  3. 因此,如果需要,只要能保证组件的依赖数据不发生变化,组件就不会重新渲染;

    • 对于原始数据类型,保持其值不变即可;
    • 对于对象类型,保持其引用不变即可;
  4. 从另一方面来说,由于可以通过保持属性引用稳定来避免子组件的重渲染,那么应该细分组件来尽量避免多余的渲染;

延迟装载

首页白屏

首页白屏时间主要受到两个因素的影响:

  1. 打包体积过大(本节不予讨论):巨型包需要消耗大量的传输时间,导致 JS 传输完成前页面只有一个 <div> ,没有可显示的内容;
  2. 需要立即渲染的内容太多:JS 传输完成后,浏览器开始执行 JS 构造页面,但可能一开始要渲染的组件太多,不仅 JS 执行的时间很长,而且执行完后浏览器要渲染的元素过多,从而导致页面白屏;

延迟装载

  1. 一个可行的办法就是 延迟装载组件 ,让组件按照指定的先后顺序依次一个一个渲染出来;

  2. 延迟装载是一个思路,本质上就是利用 requestAnimationFrame 事件分批渲染内容,它的具体实现多种多样;

  1. app.vue

    <template>
      <div class="container">
        <div class="block" v-for="n in 21" v-if="defer(n)">
          <heavy-comp></heavy-comp>
        </div>
      </div>
    </template>
    
    <script>
    import HeavyComp from './components/HeavyComp.vue';
    import defer from './mixin/defer';
    export default {
      mixins: [defer(21)],
      components: { HeavyComp },
    };
    </script>
    
    <style scoped>
    .container {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      grid-gap: 1em;
    }
    .block {
      border: 3px solid #f40;
    }
    </style>
    
  2. defer.js

    /***
     * 在 maxFrameCount 个渲染帧之内把页面渲染出来
    * @param {*} maxFrameCount 最大渲染帧
    * @returns 
    */
    export default function (maxFrameCount) {
      return {
        data() {
          return {
            frameCount: 0,
          };
        },
        mounted() {
          const refreshFrameCount = () => {
            requestAnimationFrame(() => {
              this.frameCount++;
              if (this.frameCount < maxFrameCount) {
                refreshFrameCount();
              }
            });
          };
          refreshFrameCount();
        },
        methods: {
          // 希望这个组件超过第 showInFrameCount 帧的时候渲染
          defer(showInFrameCount) {
            return this.frameCount >= showInFrameCount;
          },
        },
      };
    }
    

使用 keep-alive

长列表优化

  1. RecycleScroller.vue

    <template>
      <div class="recycle-scroller-container" @scroll="setPool" ref="container">
        <div class="recycle-scroller-wrapper" :style="{ height: `${totalSize}px` }">
          <div
            class="recycle-scroller-item"
            v-for="poolItem in pool"
            :key="poolItem.item[keyField]"
            :style="{
              transform: `translateY(${poolItem.position}px)`,
            }"
          >
            <slot :item="poolItem.item"></slot>
          </div>
        </div>
      </div>
    </template>
    
    <script>
    var prev = 10,
      next = 10;
    export default {
      props: {
        // 数据的数组
        items: {
          type: Array,
          default: () => [],
        },
        // 每条数据的高度
        itemSize: {
          type: Number,
          default: 0,
        },
        keyField: {
          // 给我的items数组中,每个对象哪个属性代表唯一且稳定的编号
          type: String,
          default: 'id',
        },
      },
      data() {
        return {
          pool: [
            // { item: 原始数据, position: 该数据对应的偏移位置 }
          ], // 渲染池,保存当前需要渲染的数据
        };
      },
      mounted() {
        this.setPool();
        window.vm = this;
      },
      computed: {
        totalSize() {
          return this.items.length * this.itemSize; // 总高度
        },
      },
      methods: {
        setPool() {
          const scrollTop = this.$refs.container.scrollTop;
          const height = this.$refs.container.clientHeight;
    
          // 开始和结束索引
          let startIndex = Math.floor(scrollTop / this.itemSize);
          let endIndex = Math.ceil((scrollTop + height) / this.itemSize);
    
          // 前面多渲染 prev 个节点
          // 后面多渲染 next 个节点
          startIndex -= prev;
          if (startIndex < 0) {
            startIndex = 0;
          }
          endIndex += next;
    
          // 偏移量
          const startPos = startIndex * this.itemSize; 
          this.pool = this.items.slice(startIndex, endIndex).map((it, i) => ({
            item: it,
            position: startPos + i * this.itemSize,
          }));
        },
      },
    };
    </script>
    
    <style>
    .recycle-scroller-container {
      overflow: auto;
    }
    .recycle-scroller-wrapper {
      position: relative;
    }
    .recycle-scroller-item {
      position: absolute;
      width: 100%;
      left: 0;
      top: 0;
    }
    </style>
    
  2. App.vue

    <template>
      <div id="app">
        <RecycleScroller :items="items" :itemSize="54" class="scroller" v-slot="{ item }">
          <ListItem :item="item" />
        </RecycleScroller>
      </div>
    </template>
    
    <script>
    import ListItem from './components/ListItem.vue';
    import RecycleScroller from './components/RecycleScroller';
    // import { RecycleScroller } from 'vue-virtual-scroller';
    // import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
    
    var items = [];
    for (var i = 0; i < 10000; i++) {
      items.push({ id: i + 1, count: i + 1 });
    }
    
    export default {
      components: { ListItem, RecycleScroller },
      data() {
        return {
          items,
        };
      },
    };
    </script>
    
    <style>
    #app {
      width: 100%;
      margin: 0 auto;
    }
    .scroller {
      width: 500px;
      margin: 0 auto;
      height: 500px;
    }
    </style>
    
打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 使用 key
  2. 2. 使用冻结对象
  3. 3. 使用函数式组件
  4. 4. 使用计算属性
  5. 5. 保持对象引用稳定
  6. 6. 延迟装载
    1. 6.1. 首页白屏
    2. 6.2. 延迟装载
  7. 7. 使用 keep-alive
  8. 8. 长列表优化