vue性能优化

前言

本文主要记录日常开发中常见的优化技巧。主要是针对2.x版本的。

函数式组件

函数式组件是使用 functional 字段来进行声明的。它是一个没有data响应式数据和this上下文,也没有生命周期钩子函数这些东西,只接受一个props。普通对象类型的组件在patch的时候,如果遇见一个节点是组件,就会递归执行子组件的的初始化话过程。而函数式组件render生成的是普通vnode,不会有递归子组件的过程,因此渲染开销会低很多。实际上可以理解成把DOM抽离了出来,是一种在DOM层面的复用。

我们可以从源码中看见:

function createComponent(Ctor, data, context, children, tag) {
  // ...
  // 根据 functional 字段来判断是否为函数式组件
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children);
  }
  // ...
  // 正常的组件是在此进行初始化方法(包括响应数据和钩子函数的执行)
  installComponentHooks(data);
  // ...
  return vnode;
}

从上面我们可以看见,在创建组件的时候,会根据functional字段来判断是否为函数式组件,是就会走函数式组件的创建过程,不是就会走正常组件的创建过程(初始化生命周期函数,响应式数据等等)。

函数式组件一般是使用在一些没有交互,不需要存储内部状态,纯展示 UI 的组件上面。比如新闻公告详情这些页面,就是单纯地把数据显示出来。

使用方式如下:

Vue.component("my-component", {
  functional: true,
  // Props 是可选的
  props: {
    // ...
  },
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文
  render: function (createElement, context) {
    // ...
  },
});

2.5.0以上的版本,还可以这样子写

<template functional></template>

冻结列表数据

在我们平常的开发中,会经常遇见一些列表的数据。这些列表数据是一个Array数组,数据的每一项又是一个普通对象,但是这些列表数据只是单纯的展示,每一项数据是不需要发生变化的。那么,我们可以使用Object.freeze([])来冻结列表数据,减少数据响应的层级(递归),提高性能。

我们可以从源码中看见:

export class Observer {
  constructor(value: any) {
    def(value, "__ob__", this);
    if (Array.isArray(value)) {
      // 将数组中的所有元素都转化为可被侦测的响应式
      this.observeArray(value);
    } else {
      // 普通对象
      this.walk(data);
    }
  }
  walk(data) {
    for (const key in data) {
      if (Object.hasOwnProperty.call(data, key)) {
        //   将普通对象转化为响应式数据
        definedRetive(data, key, data[key]);
      }
    }
  }
  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      // 监听数组的每一项
      observe(items[i]);
    }
  }
}
export function observe(value, asRootData) {
  // 如果监听的数据是一个非对象类型或者是一个vnode,则不进行监听
  if (!isObject(value) || value instanceof VNode) {
    return;
  }
  let ob;
  if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
    //   已经监听过的数据上面会有__ob__属性
    ob = value.__ob__;
  } else {
    ob = new Observer(value);
  }
  return ob;
}

从上面我们可以看出,数组里面的数据会被递归进行数据监听,如果数组中的每一项拥有更深层次的对象,这些更深层次的对象也会被递归变成响应式数据。

Object.freeze是可以将一个对象变为不可配置的,也就是只能读,也就是将configurable设置为false,不能进行增删改这些操作。vue 进行数据响应的时候,如果发现是一个不可配置的对象后,就会return返回,不会执行下面的逻辑,也就是不会把数据变成响应式数据的逻辑。

我们可以从源码中看见:

export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 获取对象的描述信息
  const property = Object.getOwnPropertyDescriptor(obj, key);
  //   configurable判断是否为可配置的
  if (property && property.configurable === false) {
    return;
  }
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // ...
    },
    set: function reactiveSetter(newVal) {
      // ...
    },
  });
}

冻结列表数据一般是使用在那些数据量大,但是又不需要对每一项数据进行修改的场景,通常这些列表数据只是用来展示。比如新闻公告列表。

代码示例:

<template>
  <div>
    <div v-for="(item, index) in list" :key="index">
      {{ item.label }}
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      list: [],
    };
  },
  created() {
    let list = [];
    for (let i = 1; i < 1000; i++) {
      list.push({
        id: i,
        label: `第${i}个`,
      });
    }
    // 冻结数据
    list = Object.freeze(list);
    this.list = list;
  },
};
</script>

子组件拆分

当我们的页面上有如下代码时:

<template>
  <div>
    首页
    {{ message }}
    {{ count }}
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
      message: "hello world",
    };
  },
  mounted() {
    this.timer = setInterval(() => {
      this.count += 1;
    }, 1000);
  },
};
</script>

从上面可以看见,该页面由于有一个定时器,所以每秒会触发一次更新。由于 vue 的更新是组件粒度的(只更新发生数据变化的组件,不会递归更新子组件),整个页面都会被重新更新,当我们的页面上还有其他比较复杂的逻辑时,这个更新过程是很耗时的(先转化为 vnode->在进行 patch 对比新旧 vnode->更新)。所以我们要把上面的代码封装成一个组件,减少重新更新的范围。代码如下

count-component 组件

<template>
  <span>
    {{ count }}
  </span>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  mounted() {
    this.timer = setInterval(() => {
      this.count += 1;
    }, 1000);
  },
};
</script>
<template>
  <div>
    首页
    {{ message }}
    <count-component />
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: "hello world",
    };
  },
};
</script>

局部变量(缓存变量)

我们先看一下下面的代码:

<template>
  <div>{{ result }}</div>
</template>

<script>
export default {
  data() {
    return {
      start: 1,
      base: 24,
    };
  },
  computed: {
    result() {
      let result = this.start;
      for (let i = 0; i < 1000; i++) {
        result +=
          this.base * this.base + this.base + this.base * 2 + this.base * 3;
      }
      return result;
    },
  },
};
</script>

从上面可以看见,result 这个计算属性在计算结果的时候会频繁访问this.base这个数据。

我们再看看 vue 中关于数据响应的源码:

function defineReactive(obj: Object, key: string, val: any) {
  const dep = new Dep();
  let childOb = observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // ...
      // getter的时候进行依赖收集
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(val)) {
            dependArray(val);
          }
        }
      }
      return val;
    },
    set: function reactiveSetter(newVal) {
      if (newVal === val) {
        return;
      }
      val = newVal;
      childOb = observe(newVal);
      dep.notify();
    },
  });
}

综合来看,在读取this.base这个属性的时候会触发它的getter,进而会执行依赖收集相关逻辑代码。result这个计算属性中,每一次 for 循环都会读取 6 次this.base属性,一共循环了 1000 次,所以getter依赖收集相关逻辑代码会被执行 6000 次。这 6000 次做的都是无用功的,从而导致性能下降了。

实际上来说,this.base只需要被读取一次,然后执行一次依赖收集就可以了。所以我们可以使用局部变量来缓存this.base属性的值,后续我们就是用这个局部变量代替this.base,就不会在走依赖收集的相关逻辑了。优化后的代码如下:

<template>
  <div>{{ result }}</div>
</template>

<script>
export default {
  data() {
    return {
      start: 1,
      base: 24,
    };
  },
  computed: {
    result({ base, start }) {
      let result = start;
      for (let i = 0; i < 1000; i++) {
        result +=
          Math.sqrt(Math.cos(Math.sin(base))) +
          base * base +
          base +
          base * 2 +
          base * 3;
      }
      return result;
    },
  },
};
</script>

在实际的开发中,我看见有很多人每次取变量的时候都是喜欢直接写this.xxx,当访问次数多了(特别是在 for 循环里面),性能的缺陷就会凸显出来了。所以当你在一个函数中频繁的读取某个变量值的时候,请记得使用局部变量来缓存变量值。

局部变量这个性能优化其实不单单可以使用在 vue 上面,还可以使用在其他地方。比如我们需要循环一个数组的时候,可以缓存数组的长度,而不是在每次循环的时候读取数组的length属性(实际上很多人喜欢在循环的直接读取数组的length属性)。操作 DOM 的时候也要把 DOM 使用局部变量缓存下来,因为 DOM 的读取是相当消耗性能的。

computed 的缓存特性

<template>
  <div>
    <div :style="style">bar1--{{ count }}</div>
    <div :style="getStyle()">bar2--{{ count }}</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      width: 100,
      count: 0,
    };
  },
  computed: {
    style() {
      console.log("style");
      return { width: `${this.width}px` };
    },
  },
  mounted() {
    setInterval(() => {
      this.count += 1;
    }, 1000);
  },
  methods: {
    getStyle() {
      console.log("getStyle");
      return { width: `${this.width}px` };
    },
  },
};
</script>

从上面我们可以看见,style计算属性返回的东西跟getStyle函数返回的东西实际上是一样的。但是当我们的定时器启动的时候,就会每一秒触发一次视图的更新。我们可以从控制台中可以看见,每一秒都会打印出一次getStyle,而style只打印了一次。这个得益于 vue 的computed计算属性具有缓存的特性,只有当width的值发生变化的时候,style这个计算属性才会重新计算,count这个属性并不是style计算属性依赖的变量,所以count的变化不会影响到count计算属性。所以我们要善于利用computed这个计算属性,而不是通过一个methods函数返回一个值,methods函数会随着每次视图更新而触发,重新执行一次。如果methods函数中包含了大量的逻辑运算,就会造成大量的性能损耗。

vue 的计算属性源码如下:

const computedWatcherOptions = { lazy: true };
function initComputed(vm: Component, computed: Object) {
  // 往组件实例上面添加一个_computedWatchers属性,保存所有computed watcher
  const watchers = (vm._computedWatchers = Object.create(null));
  // 遍历computed上面的所有属性
  for (const key in computed) {
    const userDef = computed[key];
    // computed可以是一个函数或者是对象
    const getter = typeof userDef === "function" ? userDef : userDef.get;
    // 数据响应的watcher
    watchers[key] = new Watcher(
      vm,
      getter || noop,
      noop,
      computedWatcherOptions
    );
    if (!(key in vm)) {
      defineComputed(vm, key, userDef);
    }
  }
}

function defineComputed(target: any, key: string, userDef: Object | Function) {
  if (typeof userDef === "function") {
    sharedPropertyDefinition.get = createComputedGetter(key);
    sharedPropertyDefinition.set = noop;
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop;
    sharedPropertyDefinition.set = userDef.set || noop;
  }
  // 重写get,set
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

function createComputedGetter(key) {
  // 返回的是一个`getter`
  return function computedGetter() {
    const watcher = this._computedWatchers && this._computedWatchers[key];
    // watcher存在说明computed属性存在
    if (watcher) {
      // 如果computed依赖的响应式数据发生了变化,就会触发watcher.update,把dirty置为true,重新计算computed属性
      // 如果没有发生变化,那么返回的还是上一次的值
      if (watcher.dirty) {
        // evaluate函数内部会重新获取watcher.value的值,并把watcher.dirty设置为false,下一次就不会被重新计算了
        watcher.evaluate();
      }
      return watcher.value;
    }
  };
}

function createGetterInvoker(fn) {
  return function computedGetter() {
    return fn.call(this, this);
  };
}

class Watcher {
  constructor(vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);
    if (options) {
      // 初始化为true
      this.lazy = !!options.lazy;
    }
    this.getter = expOrFn;
    // 初始化为true
    this.dirty = this.lazy;
    // 默认是undefined
    this.value = this.lazy ? undefined : this.get();
  }
  update() {
    if (this.lazy) {
      // computed依赖的数据发生变化的时候,会把dirty置为true
      this.dirty = true;
    }
  }
  evaluate() {
    // 重新获取值
    this.value = this.get();
    this.dirty = false;
  }
}

v-if 和 v-for 不要同时出现

v-for 指令是用来循环列表的。v-if是用来隐藏组件的,使用v-if隐藏的组件是不会执行内部的渲染逻辑的。我们看一下如下代码:

<template>
<div>
<div v-for='(item,index) in list' v-if='index%2===0' class='item'>{{item}}</div>
<div>
</template>
<script>
export default {
  data(){
    return {
      list:[1,2,3,4,5,6,7,8,9,10]
    }
  }
}
</script>

v-ifv-for同时出现的时候,v-for的优先级会比v-if的高。也就是说class='item'的 div 首先会被渲染成 10 个 div,然后再判断下标索引号是否为偶数,不是就隐藏掉。其中有 5 次(5 个奇数)渲染是做无用功的。5 次的无用功无疑就会造成性能上面的浪费。所以我们可以借助computed先过滤掉那些不需要显示的数据,然后在使用v-for循环列表。代码如下:

<template>
<div>
<div v-for='(item,index) in showList' class='item'>{{item}}</div>
<div>
</template>
<script>
export default {
  data(){
    return {
      list:[1,2,3,4,5,6,7,8,9,10]
    }
  },
  computed:{
    showList(){
      const list = this.list
      return list.filter((item,index)=>index%2===0)
    }
  }
}
</script>

有时候我们需要根据某个字段来控制列表是否显示,代码如下:

<template>
<div>
<div v-for='(item,index) in list' v-if='show'>{{item}}</div>
<div>
</template>
<script>
export default {
  data(){
    return {
      list:[1,2,3,4,5,6,7,8,9,10],
      show:false
    }
  }
}
</script>

从上面可以看见,show 为 false,也就意味着做了 10 次没有意义的渲染。我们可以将v-forv-if指令分离,让v-if先执行,这样就不会做 10 次无意义的渲染了。代码如下:

<template>
<div>
<template v-if='show'>
<div v-for='(item,index) in list' >{{item}}</div>
</tempalte>
<div>
</template>
<script>
export default {
  data(){
    return {
      list:[1,2,3,4,5,6,7,8,9,10],
      show:false
    }
  }
}
</script>

我们来看看 vue 的源码:

function genElement(el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre;
  }

  if (el.staticRoot && !el.staticProcessed) {
    // 静态节点
    return genStatic(el, state);
  } else if (el.once && !el.onceProcessed) {
    // v-once指令
    return genOnce(el, state);
  } else if (el.for && !el.forProcessed) {
    // v-for指令
    return genFor(el, state);
  } else if (el.if && !el.ifProcessed) {
    // v-if指令
    return genIf(el, state);
  } else if (el.tag === "template" && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || "void 0";
  } else if (el.tag === "slot") {
    return genSlot(el, state);
  } else {
    // ...
  }
}

从上面的 if-else 判断条件中,我们可以看见v-for的执行要优先于v-if

不需要渲染在视图的数据不要写在 data 中

渲染在视图的数据是指在<template></template>html 模板中使用到的数据,这些数据都是响应式数据来的。而定义在 data 字段中的数据都会变成响应式数据,但是有些数据我们是不需要显示在视图中的,就不要把数据声明在 data 中了,比如在移动端中进行滚动加载,需要使用到分页参数,这些分页参数就不应该写在 data 中的(实际上就我看见的,很多人喜欢吧分页参数pageSizepageIndex写在 data 中的)。

优化前代码如下:

export default {
  data() {
    return {
      pageSize: 10,
      pageIndex: 1,
    };
  },
  methods: {
    getList() {
      axios
        .get("xxx", {
          params: { pageSize: this.pageSize, pageIndex: this.pageIndex },
        })
        .then(() => {});
    },
    scrollBottom() {
      this.pageIndex += 1;
      this.getList();
    },
  },
};

上面我们可以看见,pageSizepageIndex被定义在了data中,这就意味着这 2 个数据将会变成响应式数据,但是实际上pageSizepageIndex不需要像是在视图中。当我们对pageSizepageIndex就进行读操作时候,就会走getter依赖收集的逻辑,进行写操作的时候setter通知更新的逻辑,由于不需要反馈到视图中,所以gettersetter中的逻辑就是在做无用功,损耗了性能。

优化后代码如下:

export default {
  created() {
    this.pageSize = 10;
    this.pageIndex = 1;
  },
  methods: {
    getList() {
      axios
        .get("xxx", {
          params: { pageSize: this.pageSize, pageIndex: this.pageIndex },
        })
        .then(() => {});
    },
    scrollBottom() {
      this.pageIndex += 1;
      this.getList();
    },
  },
};

从上面可以看见,我们把pageSizepageIndex挂在到了实例this上面,这样既可以在组件的每个函数中访问到,又可以避免把它们变成响应式数据。

我们看看 vue 的源码:

function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  // 判断有没有data字段
  if (opts.data) {
    // 初始化data字段中数据
    initData(vm);
  } else {
    observe((vm._data = {}), true /* asRootData */);
  }
}

function initData(vm: Component) {
  let data = vm.$options.data;
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
  const keys = Object.keys(data);
  let i = keys.length;
  while (i--) {
    const key = keys[i];
    // 把data中的数据代理到this上
    proxy(vm, `_data`, key);
  }
  // 将data中的数据转化为响应式数据
  observe(data, true /* asRootData */);
}

从上面可以看见,在initData的时候会把data中的数据代理到this上面,同时转化为响应式数据。所以一些需要共享的数据,但是不需要响应的数据,不要写在data字段中,否则会在数据进行读写的时候进行无用功操作,我们可以在适当的时机直接在组件实例this上面挂载一些变量,从而提高性能。

v-for 中的 key

在 v-for 中,我们设置 key 是为了给 vnode 一个唯一的标识,标识新旧 vnode 是否为同一个 vnode,快速找到新旧 vnode 的变化。当我们不设置的时候,默认就是undefined,也就是 v-for 中的每一项都是相同的 key 值。

我们先看一下 vue 源码中sameVnode函数的逻辑:

function sameVnode(a, b) {
  return (
    a.key === b.key &&
    a.asyncFactory === b.asyncFactory &&
    ((a.tag === b.tag &&
      a.isComment === b.isComment &&
      isDef(a.data) === isDef(b.data) &&
      sameInputType(a, b)) ||
      (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
  );
}

上面可以看出,

  • 判断新旧 vnode 是否为同一个 vnode 的时候,首先会判断新旧 vnode 中的key值是否相同,不同就是不同的 vnode,相同就继续走后面更加复杂逻辑判断。

  • 当我们不设置 v-for 中的 key 值的时候,也就是 key 值为undefined,即a.key === b.key->undefined === undefined->true,不管新旧 vnode 是否为相同还是不相同的 vnode,都会走后面更加复杂的逻辑判断。

  • 当我们设置了key值,并且每个key都是不一样的,当新旧 vnode 不是相同的 vnode 的时候,a.key === b.key->false,在对比 key 值的时候就可以直接返回false了,不用走后面更加复杂的逻辑判断,只有当新旧 vnode 相同的时候,也就是a.key === b.key->true,才会走后面的更加复杂的逻辑判断。

  • 设置key相对于不设置key,主要是减少了a.key === b.key后面的复杂逻辑判断流程,从而减少了 js 执行的时间。

从上面中,我们可以总结出,key的作用是为了更加快速地对比出两个是不同的vnode,而相同的vnode,因为我们可以看见在对比完key值之后,后面还有一些判断条件

当我们设置 key 值的时候,要保证key值的唯一性,最好不要用数组的下标索引作为key值,可能会产生一些 bug,我们看看下面的使用数组下标索引作为key值的代码

list-card.vue

<template>
  <div :class="{ red: isClick }" @click="isClick = true">
    {{ label }}
  </div>
</template>
<script>
export default {
  props: ["label"],
  data() {
    return {
      isClick: false,
    };
  },
};
</script>
<style>
.red {
  color: red;
}
</style>

app.vue

<template>
  <div id="app">
    <list-card v-for="(item, index) in list" :key="index" :label="item" />
    <button @click="onClick">添加</button>
  </div>
</template>
<script>
import ListCard from "./component/list-card.vue";
export default {
  name: "App",
  components: {
    ListCard,
  },
  data() {
    return {
      list: [1, 2, 3, 4, 5, 6],
    };
  },
  methods: {
    onClick() {
      this.list.unshift(this.list.length + 1);
    },
  },
};
</script>

上面我们可以看见list-card组件内部自己维护了一个isClick变量,当点击组件的时候,组件字体会变成红色。当我们点击添加按钮的时候,会在数组的顶部添加元素。现在我们进行如下操作:

  • 点击第三个元素,也就是 label=3 的组件,此时, label=3 的组件字体变红色

  • 点击添加按钮,往数组顶部追加元素,此时list数组变成[7,1, 2, 3, 4, 5, 6]

  • 这时我们理想中的效果应该是第四个元素,也就是label=3 的组件应该是红色字体的。但是实际的效果是,第三个元素,也就是label=2 的组件变成了红色字体

keep-alive

在实际的开发过程中,我们每次渲染路由的时候,都会重新渲染一次组件,都会经过renderpatch等过程,如果组件逻辑比较复杂,或者嵌套比较深,整个渲染过程都会很耗时。

我们可以使用keep-alive组件将router-view包裹起来,这样就可以将已经渲染过的路由组件缓存起来,下次需要渲染组件的时候,就不会走createdmounted等生命周期函数,而是会触发activated(激活)和deactivated(失活)生命周期函数。keep-alive不仅可以用来缓存路由组件,还可以用来缓存动态组件。这是一种使用空间换取时间的优化方式。

keep-alive主要参数有如下三个:

  • include:字符串或正则表达式,也可以使用数组来指定换缓存那些组件
  • exclude:字符串或正则表达式,也可以使用数组来指定换不缓存那些组件
  • max:最大缓存的组件实例

注意:当你使用了includeexclude参数时,组件必须要有name选项。没有name选项就会去匹配它的局部注册名称 (父组件 components 选项的键值)。匿名组件不能被匹配。

代码示例:

<template>
  <!-- 逗号分隔字符串 -->
  <keep-alive include="a,b">
    <router-view></router-view>
  </keep-alive>

  <!-- 正则表达式 (使用 `v-bind`) -->
  <keep-alive :include="/a|b/">
    <component :is="view"></component>
  </keep-alive>

  <!-- 数组 (使用 `v-bind`) -->
  <keep-alive :include="['a', 'b']">
    <component :is="view"></component>
  </keep-alive>
</template>

常用的场景是在移动端中,从一个列表中,跳转到一个详情页,再从详情页返回到列表的时候,需要恢复到列表上一次进入到详情页时的状态。

渐进式渲染

渐进式渲染就是分批延时渲染组件,就是先渲染组件 A,等过了几秒之后就渲染组件 B。当一个页面存在非常多的组件的时候,浏览器在同一时间内需要渲染的 DOM 比较多,会给人一种页面卡顿的感觉。

我们来看看怎么实现分批延时渲染

<template>
  <div>
    <component-a v-if="defer(1)"></component-a>
    <component-b v-if="defer(5)"></component-b>
  </div>
</template>
<script>
const count = 10;
export default {
  data() {
    return {
      displayPriority: 0,
    };
  },
  mounted() {
    this.startDisplayPriority();
  },
  methods: {
    startDisplayPriority() {
      const step = () => {
        requestAnimationFrame(() => {
          this.displayPriority++;
          if (this.displayPriority < count) {
            step();
          }
        });
      };
      step();
    },
    defer(priority) {
      return this.displayPriority >= priority;
    },
  },
};
</script>

我们可以看见组件内部维护了一个displayPriority变量,然后通过requestAnimationFrame在每一帧渲染的时候自增,最多自增到count。然后就可以通过v-if="defer(xxx)"来控制渲染的先后顺序,xxx数值越小,渲染优先级越高。这里只能用v-if来控制渲染的优先级,不能使用v-show,因为v-show会直接渲染组件的,只是在样式上面做了隐藏。

当你的页面上存在非常多的组件,或者有非常耗时的组件时,使用这种渐进式的渲染方式可以避免由于 js 执行时间过长或者一次性渲染出大量的 DOM 元素时导致渲染卡住的现象,提高了用户的体验度。

时间片切割

当我们一次性提交很多数据的时候,内部的 js 执行时间过长,阻塞了 UI 线程,导致页面卡死。

通常来说我们在提交大量数据的时候回加一个loading效果,但是但数据量很大的时候,由于 js 执行时间过长,UI 线程无法执行,导致我们的loading效果根本就是静态效果,不是动态的。这是我们可以拆成多个时间片去提交数据,使得单次 js 运行的时间变短,这样就不会阻塞了 UI 线程,我们的loading效果也有机会动起来了。

我们来看看这么实现时间片切割技术

function splitArray(items, splitCount) {
  const arr = [];
  const count = Math.ceil(items.length / splitCount);
  for (let i = 0; i < count; i++) {
    arr.push(items.slice(i * splitCount, (i + 1) * splitCount));
  }
  return arr;
}
    fetchData({ commit }, { list, splitCount }) {
      // 将数据切分成多个数据块
      const chunkList = splitArray(list,splitCount);
      const step = (index) => {
        if (index >= chunkList.length) {
          return;
        }
        // requestAnimationFrame在浏览器每一次重绘之后会执行传入的回调函数,这样就使得,UI线程有机会执行。当然也可以使用setTimeout来代替requestAnimationFrame
        requestAnimationFrame(() => {
        // 结合requestAnimationFrame每一帧提交一部分数据
          const chunk = chunkList[index];
          commit('addItems',chunk)
          index += 1;
          step(index);
        });
      };
      step(0);
    }

组件懒加载

组件懒加载是将异步组件和 webpack 的 code-splitting 功能一起配合使用的。这样就可以将组件代码切割成多个文件,需要真正使用组件的时候才去请求加载代码。

代码示例:

const comp = (/* webpackChunkName: "comp" */)=>import('./my-component')

Vue.Component('comp',comp)

上面我们可以看见有一个webpackChunkName: "comp"的参数,这个参数是用来指定代码块的文件名称,也就是打包出来的文件名叫comp。这里我建议大家把相同模块的组件都打包进同一个文件中(只要webpackChunkName的值相同即可打包进同一个文件中),实际上我看见很多人喜欢一个组件就是一个文件,打包出来的文件实际上只有几kb,发送网络请求去请求文件携带的请求头,请求体等数据可能都比这个文件体积大,会造成不必要的浪费。

总结

通过这篇文章,我希望大家可以把这些优化技巧运用到实际项目开发中,这可以给你带来不少的收益。当然除了上述的优化技巧,还有图片懒加载,虚拟滚动等性能优化手段。

性能优化并不能盲目的去做,你要结合当前实际,考虑这个优化能带来什么,有什么收益,需要花费多少成本。同时还要考虑可能会带来的风险,比如上面keep-alive,就是采用空间来换取时间,一旦缓存的组件数量多了,内存容易造成溢出,所以要控制缓存组件数量。

性能的提升是靠一点一滴去积累的,并不是说你做了某个优化之后,就可以突然把渲染时间从5秒降低到3秒。其实在实际开发中,有很多地方我们都可以去进行优化的,只是可能你认为这样无所谓,影响不大,可以忽略不计。但是等项目到了一定规模之后,这些看似微不足道的地方就会被积累起来,量变形成质变,导致系统的性能下降。所以很多你看似无所谓,影响不大的地方,往往就是性能瓶颈的突破口。

最后,我希望大家在日常开发中,不要为了写而写,你要去充分考虑你写的这一行代码会产生什么样的性能影响。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值