# Vue的常用优化点

# v-if/v-show

频繁切换适合使用v-show,减少dom节点的销毁和重建

  • v-if 如果初始条件为假,不执行编译渲染,直到首次为真时,才会进行局部编译,v-show始终会被编译保留,
  • 切换显示时,使用v-if的组件,会被移出dom树,生命周期每次都会被触发,v-show是样式的显示与隐藏,生命周期只会执行一次

# 函数式组件

# 介绍

一个无状态和无实例(没有this)的组件,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法(没有钩子函数,没有响应式数据),它只是一个接受一些 prop 的函数:

  • Stateless(无状态):组件自身是没有状态的
  • Instanceless(无实例):组件自身没有实例,也就是没有this

# v2/v3 差异

在v2中函数式组件常用于:

  • 性能优化,它们的初始化速度比有状态组件快得多
  • 可以返回多个根节点

v3中:

  • 对比v2,函数式组件的性能提升可以忽略不计
  • 有状态组件的组件支持了返回多节点
  • 有状态组件的性能已经有了极大的提升
  • functional attribute 在 <template> 中移除
  • listeners 现在作为 $attrs 的一部分传递,可以将其删除

# v2用法

使用渲染函数

export default {
  functional: true,
  props: ['level'],
  render(h, { props, data, children }) {
    return h(`h${props.level}`, data, children)
  }
}
1
2
3
4
5
6
7

使用template

<template functional>
  <component
    :is="`h${props.level}`"
    v-bind="attrs"
    v-on="listeners"
  />
</template>

<script>
export default {
  props: ['level']
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

# v3用法

import { h } from 'vue'
const DynamicTitle = (props, context) => {
  return h(`h${props.level}`, context.attrs, context.slots)
}
DynamicTitle.props = ['level']
export default DynamicHeading

1
2
3
4
5
6
7

单文件组件

在 3.x 中,有状态组件和函数式组件之间的性能差异已经大大减少,并且在大多数用例中是微不足道的。因此,在 SFCs 上使用 functional 的开发人员的迁移路径是删除该 attribute,并将 props 的所有引用重命名为 $props,将 attrs 重命名为 $attrs。

 

 
 









<template>
  <component
    v-bind:is="`h${$props.level}`"
    v-bind="$attrs"
  />
</template>

<script>
export default {
  props: ['level']
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12

主要的区别在于:

  1. functional attribute 在 <template> 中移除
  2. listeners 现在作为 $attrs 的一部分传递,可以将其删除

# this的使用与替代

# 说明

vue会对数据进行响应式处理,添加setter/getter:

  • 读取值时,调用getter方法,收集依赖(watch/computed/模板渲染)
  • 设置值时,调用setter方法,触发
// observer.js
function defineReactive (obj: Object,key: string,val: any,customSetter?: ?Function,shallow?: boolean) {

  const dep = new Dep()

  Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        const value = getter ? getter.call(obj) : val
        if (Dep.target) {
          dep.depend()
          if (childOb) {
            childOb.dep.depend()
            if (Array.isArray(value)) {
              dependArray(value)
            }
          }
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        const value = getter ? getter.call(obj) : val
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (process.env.NODE_ENV !== 'production' && customSetter) {
          customSetter()
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) return
        if (setter) {
          setter.call(obj, newVal)
        } else {
          val = newVal
        }
        childOb = !shallow && observe(newVal)
        dep.notify()
      }
    })
}
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
// dep.js
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
1
2
3
4
5
6

频繁使用this.获取属性值,就会多次getter方法,收集依赖(实际收集时,会记录依赖id,不会重复收集的依赖),重复调用setter方法就存在了性能损耗

 // watcher.js 
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
1
2
3
4
5
6
7
8
9
10
11

# 优化方法

使用局部变量或者结构赋值,减少依赖收集方法执行次数




 


 





{
  computed: {
    msg() {
      const data = this.data
      return `${data}` 
    },
    count({num}) {
      return num + num
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11

整合模板中使用的数据

<template>
  <ul>
    <!-- 整合前 -->
    <li v-for="(item, index) in list" :key="index">
      {{arr[index].name}} - {{item.id}} - {{obj[item.id].num}}
    </li>
    <!-- 整合后 -->
    <li v-for="(item, index) in listData" :key="index">
      {{item.name}} - {{item.id}} - {{item.num}}
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      list: [],
      obj: {},
      arr: []
    },
    computed: {
      listData ({list, arr, obj}) {
        list.forEach(item => {
          item.name = arr[item.index].name
          item.num = obj[item.id].num
        })
        return list
      }
    }
  }
}
</script>
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

# keep-alive

  • <keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
  • <transition> 相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。
<keep-alive :include="cachedViews">
  <router-view :key="key" />
</keep-alive>
1
2
3

<keep-alive> 要求同时只有一个子元素被渲染

# 异步组件

在大型应用中,我们可以将应用分割成小一些的代码块,并且只在需要的时候(延迟加载)才从服务器加载一个模块。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。

异步组件可以设置loading的选项,在nuxt服务器端渲染的时候我们只输出这个loading组件,后续渲染异步组件的时候,就可以减少回流所带来的性能消耗。 另外组件懒加载了之后,原本的图片懒加载也可以考虑是否要去掉。

const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})
1
2
3
4
5
6
7
8
9
10
11
12
13

# 分割long task

浏览器GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。因此long task会影响TTI,SPA页面的FCP,或者造成动画延迟,响应延迟,我们需要找出long task,拆分task,设置优先级去执行task。

可以通过performance配合TimingsMain,找到长时间占用主线程的task,并且分析是由哪些方法组成的。尝试将他拆分后,考虑按优先级去执行他。

优化前

longTask({ commit }, { list }) {
    commit('clearLists');
    commit('addLists', list)
    commit('calculateLists', list)
}
1
2
3
4
5

优化后

longTask({ commit }, { list, splitCount }) {
  commit('clearLists')
  commit('clearChunks')
  const queue = new Queue()
  // 分割列表
  splitList(list, splitCount).forEach(chunk => queue.add(done => {
    // 将分割后的任务逐个添加到队列
    requestAnimationFrame(() => {
      commit('addLists', chunk)
      commit('calcuChunk', chunk)
      done()
    })
  }))
  // 统计分割后计算结果
  queue.add(done => {
    requestAnimationFrame(() => {
      commit('countChunk')
      done()
    })
  })
  // 执行队列
  awiat queue.start()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# defer延迟加载组件

组件渲染时,同样会执行大量计算或者复杂的渲染,也可以利用requestAnimationFrame,延迟组件加载

<template>
  <div>
    <h2>I'm an heavy page</h2>

    <template v-if="defer(2)">
      <Heavy v-for="n in 10" :key="n"/>
    </template>

    <Heavy v-if="defer(3)" class="super-heavy" :n="9999999"/>
  </div>
</template>

<script>
import Defer from '@/mixins/Defer'

export default {
  mixins: [
    Defer()
  ]
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// @/mixins/Defer.js
export default function (count = 10) {
  return {
    data () {
      return {
        displayPriority: 0
      }
    },

    mounted () {
      this.runDisplayPriority()
    },

    methods: {
      runDisplayPriority () {
        const step = () => {
          requestAnimationFrame(() => {
            this.displayPriority++
            if (this.displayPriority < count) {
              step()
            }
          })
        }
        step()
      },

      defer (priority) {
        return this.displayPriority >= priority
      }
    }
  }
}
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

# 分割子组件

将复杂的耗时计算,放在子组件中处理

优化前

<template>
  <div :style="{ opacity: number / 300 }">
    <div>{{ heavy() }}</div>
  </div>
</template>

<script>
export default {
  props: ['number'],
  methods: {
    heavy () {
      const n = 100000
      let result = 0
      for (let i = 0; i < n; i++) {
        result += Math.sqrt(Math.cos(Math.sin(42)))
      }
      return result
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

优化后

<template>
  <div :style="{ opacity: number / 300 }">
    <ChildComponents />
  </div>
</template>

<script>
export default {
  props: ['number'],
  components: {
    ChildComponents: {
      methods: {
        heavy () {
          const n = 100000
          let result = 0
          for (let i = 0; i < n; i++) {
            result += Math.sqrt(Math.cos(Math.sin(42)))
          }
          return result
        }
      },
      render(h) {
        return h('div', this.heavy())
      }
    }
  }
}
</script>

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

# 使用非响应式数据

数据冻结,减少响应式劫持

// 方法一
const data = items.map(item => optimizeItem(item))
function optimizeItem (item) {
  Object.defineProperty(item, 'data', {
    // mark as non-reactive
    configurable: false,
    value: item.data
  })
  return item
}
// 方法二
this.items = Object.freeze(items)
1
2
3
4
5
6
7
8
9
10
11
12

# 渲染可视化部分

常用于大量数据的渲染,模拟滚动效果,仅展示可视区域内容

# await/Promise.all

没有依赖关系的任务或者请求,可以使用Promise.all处理

const [users, imgs] = await Promise.all([getUsers(), getImgs()])
1

# 文献参考