跳到主要内容

Vue-技巧和性能

使用技巧

监听组件的生命周期

常规做法:

// Parent.vue
<Child @mounted="doSomething"/>

// Child.vue
mounted() {
this.$emit("mounted");
}

简约做法:

<Child @hook:mounted="doSomething" />
<Child @hook:updated="doSomething" />

路由参数变化组件不更新的解决方案

  1. watch监听路由变化

     // 方法1 监听路由是否变化
    watch: {
    '$route' (to, from) {
    if(to.query.id !== from.query.id){
    this.id = to.query.id;
    this.init();//重新加载数据
    }
    }
    }
    //方法 2 设置路径变化时的处理函数
    watch: {
    '$route': {
    handler: 'init',
    immediate: true
    }
    }
  2. 路由key

     <router-view :key="$route.fullpath"></router-view>

路由懒加载的三种方式

// 1、Vue异步组件技术:
{
path: '/home',
name: 'Home',
component: resolve => reqire(['path路径'], resolve)
}

// 2、es6提案的import()
const Home = () => import('path路径')

// 3、webpack提供的require.ensure()
{
path: '/home',
name: 'Home',
component: r => require.ensure([],() => r(require('path路径')), 'demo')
}

require.context()

require.context(directory,useSubdirectories,regExp)

  • directory:说明需要检索的目录
  • useSubdirectories: 是否检索子目录
  • regExp: 匹配文件的正则表达式,一般是文件名

使用场景

页面需要导入多个组件:

import titleCom from '@/components/home/titleCom'
import bannerCom from '@/components/home/bannerCom'
import cellCom from '@/components/home/cellCom'

// ...
components: {
titleCom, bannerCom, cellCom
}

利用require.context:

const path = require('path')
const files = require.context('@/components/home', false, /\.vue$/)
const modules = {}
files.keys().forEach(key => {
const name = path.basename(key, '.vue')
modules[name] = files(key).default || files(key)
})

// ...
components: modules

性能优化

利用好函数式组件

优化前代码:

<template>
<div class="cell">
<div v-if="value" class="on"></div>
<section v-else class="off"></section>
</div>
</template>

<script>
export default {
props: ['value'],
}
</script>

优化后:

<template functional>
<div class="cell">
<div v-if="props.value" class="on"></div>
<section v-else class="off"></section>
</div>
</template>

这样的技巧可以减少js的执行时间. 函数式组件和普通的响应式组件不同, 它不会有状态, 也不会有响应式的数据, 生命周期钩子函数等. 可以当做一个生成DOM的Function.

其基本原理就是监听滚动事件, 动态更新需要显示的DOM元素, 计算出它们在视图中的位移.

子组件拆分

优化前:

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

<script>
export default {
components: {
ChildComp: {
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())
}
}
},
props: ['number']
}
</script>

优化后:

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

<script>
export default {
components: {
ChildComp: {
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())
}
}
},
props: ['number']
}
</script>

优化前的组件通过heavy函数模拟了一个耗时的任务, 并且这个函数会在每次渲染的时候都执行一次, 所以每次组件的渲染都会消耗比较长的时间执行js, 优化后把这个任务封装在子组件childcomp中, 由于vue的更新是组件粒度的, 虽然每一帧都通过数据修改了父组件的重新渲染, 然是childcomp却不会重新渲染, 因此内部没有使用任何响应式的数据变化, 所以优化后的组件不会再每次渲染都执行耗时任务, 自然性能就提高了. 同样的, 计算属性也能满足这种情况的性能优化.

使用局部变量

优化前:

<template>
<div :style="{ opacity: start / 300 }">{{ result }}</div>
</template>

<script>
export default {
props: ['start'],
computed: {
base () {
return 42
},
result () {
let result = this.start
for (let i = 0; i < 1000; i++) {
result += Math.sqrt(Math.cos(Math.sin(this.base))) + this.base * this.base + this.base + this.base * 2 + this.base * 3
}
return result
},
},
}
</script>

优化后:

<template>
<div :style="{ opacity: start / 300 }">{{ result }}</div>
</template>

<script>
export default {
props: ['start'],
computed: {
base () {
return 42
},
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>

这里主要优化了computed的使用, 优化前的组件会多次访问this.base, 优化后会在计算前先用局部变量base, 缓存了this.base的值, 后面直接访问base.

能优化的理由是: this.base实际上是在访问getter, 将其求值的结果返回给局部变量base, 后面再次访问就不会触发getter了, 也不会走依赖收集的逻辑了.

合理的 v-show

优化前:

<template functional>
<div class="cell">
<div v-if="props.value" class="on">
<Heavy :n="10000"/>
</div>
<section v-else class="off">
<Heavy :n="10000"/>
</section>
</div>
</template>

优化后:

<template functional>
<div class="cell">
<div v-show="props.value" class="on">
<Heavy :n="10000"/>
</div>
<section v-show="!props.value" class="off">
<Heavy :n="10000"/>
</section>
</div>
</template>

优化前后的主要区别在于使用v-show替代了v-if. 虽然逻辑相似, 但是其内部实现差距还是比较大的.

v-if指令在编译阶段会编译为一个三元运算符, 条件渲染, 比如优化前的组件模板编译之后是这样的:

function render() {
with(this) {
return _c('div', {
staticClass: "cell"
}, [(props.value) ? _c('div', {
staticClass: "on"
}, [_c('Heavy', {
attrs: {
"n": 10000
}
})], 1) : _c('section', {
staticClass: "off"
}, [_c('Heavy', {
attrs: {
"n": 10000
}
})], 1)])
}
}

props.value的值变化的时候, 会触发对应的组件更新, 对于v-if渲染的节点, 由于新旧节点的vnode不一致, 在核心的diff算法中, 会移除旧的vnode节点, 创建新的vnode节点, 会重新触发组件的初始化, 渲染, patch等过程.

但是使用v-show, 则不同, 其组件模板编译后如下:

function render() {
with(this) {
return _c('div', {
staticClass: "cell"
}, [_c('div', {
directives: [{
name: "show",
rawName: "v-show",
value: (props.value),
expression: "props.value"
}],
staticClass: "on"
}, [_c('Heavy', {
attrs: {
"n": 10000
}
})], 1), _c('section', {
directives: [{
name: "show",
rawName: "v-show",
value: (!props.value),
expression: "!props.value"
}],
staticClass: "off"
}, [_c('Heavy', {
attrs: {
"n": 10000
}
})], 1)])
}
}

props.value的值变化的时候, 会触发对应的组件更新, 对于v-show渲染的节点, 由于新旧节点是一样的, 它们只需要一直patchVnode就行了, 其内部原理大概是:

patchVNode的过程中, 内部对执行v-show指令对应的钩子函数update, 然后根据v-show的值设置DOM元素的display的值.

相比于v-if不断的删除和创建函数的新的DOM, v-show仅仅控制DOM的显示和隐藏.

两个指令都有不同的使用场景, 需要根据合适的需求使用

Deferred features 分批渲染组件

优化前:

<template>
<div class="deferred-off">
<VueIcon icon="fitness_center" class="gigantic"/>

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

<Heavy v-for="n in 8" :key="n"/>

<Heavy class="super-heavy" :n="9999999"/>
</div>
</template>

优化后:

<template>
<div class="deferred-on">
<VueIcon icon="fitness_center" class="gigantic"/>

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

<template v-if="defer(2)">
<Heavy v-for="n in 8" :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>

其中@mixin/Defer的工作原理如下:

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
}
}
}
}

Defer的主要思想是把一个组件的一次渲染拆分为多次, 它内部维护了displayPriorty, 然后通过requestAnimationFrame在每一帧渲染的时候自增, 最多加到count, 然后使displayPriority增加到x的时候渲染某些区块.

当你有渲染耗时的组件, 使用Deferred做渐进式渲染是不错的方式.

Time slicing

优化前:

fetchItems ({ commit }, { items }) {
commit('clearItems')
commit('addItems', items)
}

优化后:

fetchItems ({ commit }, { items, splitCount }) {
commit('clearItems')
const queue = new JobQueue()
splitArray(items, splitCount).forEach(
chunk => queue.addJob(done => {
// 分时间片提交数据
requestAnimationFrame(() => {
commit('addItems', chunk)
done()
})
})
)
await queue.start()
}

优化后, 我们可以将大量数据进行拆分, 这种情况下, 就能避免页面直接卡死.

Non-reactive data

使用非响应式数据, 优化前:

const data = items.map(
item => ({
id: uid++,
data: item,
vote: 0
})
)

优化后:

const data = items.map(
item => optimizeItem(item)
)

function optimizeItem (item) {
const itemData = {
id: uid++,
vote: 0
}
Object.defineProperty(itemData, 'data', {
// Mark as non-reactive
configurable: false,
value: item
})
return itemData
}

优化后的代码执行时间会好于优化前的, 这是因为在内部提交数据的时候, 会默认把新提交的数据也定义成响应式的, 如果数据的子属性是对象形式, 还会递归的让子属性也变为响应式, 因此当提交的数据很多的嘶吼, 这个过程就变成了一个耗时的过程.

而优化后我们把数据对象变为不可配置, 这样内部在walk获取对象属性的时候就会忽略这个dtata, 减少响应式的逻辑.

类似这种还有直接挂在变量的方式:

export default {
created() {
this.scroll = null
},
mounted() {
this.scroll = new BScroll(this.$el)
}
}

这样就可以在上下文中访问scroll, 又不进行响应式的监听.

Virtual Scrolling 虚拟列表

优化前:

<div class="items no-v">
<FetchItemViewFunctional
v-for="item of items"
:key="item.id"
:item="item"
@vote="voteItem(item)"
/>
</div>

优化后:

<recycle-scroller
class="items"
:items="items"
:item-size="24"
>
<template v-slot="{ item }">
<FetchItemView
:item="item"
@vote="voteItem(item)"
/>
</template>
</recycle-scroller>

使用 Keep-Alive 缓存组件状态

在组件之间切换的时候, 可以使用该API来避免组件来回切换重复渲染导致的问题. 使用keep-alive缓存组件可以让组件在被移除的时候缓存器Vnode信息, 而不是直接释放他们.

<keep-alive>
<component v-bind:is="currentComponent" class="tab"></component>
</keep-alive>

参考链接