Vue进阶-深度性能优化

前言

  • 什么时候需要优化性能

当你的项目规模扩大到一定程度,你会自然感受到项目运行明显缓慢。虽然在生产环境通常比开发环境流畅,但优化性能也可以提升开发效率,而不仅仅是为了生产环境下的体验。

  • 前提准备
  1. 你需要先保证项目的规范化,这一步很重要,不规范的项目代码不仅仅会影响可读性,也有可能会影响运行速度,严重违反规范的代码甚至可能是性能低下的元凶。如果你还不知道怎么编写规范化的Vue,请查看我的往期文章
  2. 你需要掌握一定的Vue渲染机制,这对于理解为什么会卡顿至关重要。这方面的文章已经数不胜数,但许多人学习渲染机制只是为了应付面试而死记硬背,这对优化性能没有帮助。因此确保你真正理解渲染机制很重要。
  • 本文的演示代码将在Github公开,查看演示代码的运行结果在Github pages

正文

Vue自身的优化已经足够高效,大多数情况下,Vue只会更新已经修改的部分,除了一些边缘情况。

组件会在他依赖的数据(出现在<template>里的数据)发生更改时重新渲染。此时会对整个<template>的内容重新渲染。

<script setup lang="ts">
import { ref } from 'vue'

const data1 = ref('data1')
const data2 = ref(1)

function timeConsumingProcessing(value: string) {
  console.log('耗时操作正在执行')
  // 模拟耗时操作
  const start = Date.now()
  while(Date.now() - start <= 1000) {
  }
  return value
}

const update = () => {
  data2.value++
}

</script>

<template>
  <div>
    data1:{{ timeConsumingProcessing(data1) }}
  </div>
  <div>
    data2:{{ data2 }}
  </div>
  <button @click="update">
    update
  </button>
</template>

在这个例子中,data2的更新会导致整个组件的重新渲染,即使data1没有更改。

使用computed对数据进行缓存

<script setup lang="ts">
import { computed, ref } from 'vue'

const data1 = ref('data1')
const data2 = ref(1)

function timeConsumingProcessing(value: string) {
  console.log('耗时操作正在执行')
  // 模拟耗时操作
  const start = Date.now()
  while(Date.now() - start <= 1000) {
  }
  return value
}

const processed = computed(() => timeConsumingProcessing(data1.value))

const update = () => {
  data2.value++
}

</script>

<template>
  <div>
    data1:{{ processed }}
  </div>
  <div>
    data2:{{ data2 }}
  </div>
  <button @click="update">
    update
  </button>
</template>

此时点击update,timeConsumingProcessing将不会被调用。

但如果使用的是一个引用数据类型,更改了数据的引用,但不影响实际渲染的值。

<script setup lang="ts">
import { onBeforeUpdate, onUpdated, ref } from 'vue'

const data = ref({ key: 'value' })

const update = () => {
  data.value = { key: 'value' }
}

function timeConsumingProcessing(value: string) {
  // 模拟耗时操作
  const start = Date.now()
  while(Date.now() - start <= 1000) {
  }
  return value
}

onBeforeUpdate(() => {
  console.log('组件开始更新')
})

onUpdated(() => {
  console.log('组件已更新')
})
</script>

<template>
  {{ timeConsumingProcessing(data.key) }}
  <button @click="update">update</button>
</template>

这个例子中,修改了data的引用,实际需要的data.key并没有改变,但还是会触发组件的渲染.

再次使用computed对数据进行缓存

<script setup lang="ts">
import { computed, onBeforeUpdate, onUpdated, ref } from 'vue'

const data = ref({ key: 'value' })

const update = () => {
  data.value = { key: 'value' }
}

function timeConsumingProcessing(value: string) {
  console.log('耗时操作正在执行')
  // 模拟耗时操作
  const start = Date.now()
  while(Date.now() - start <= 1000) {
  }
  return value
}

const processed = computed(() => timeConsumingProcessing(data.value.key))

onBeforeUpdate(() => {
  console.log('组件开始更新')
})

onUpdated(() => {
  console.log('组件已更新')
})
</script>

<template>
  {{ processed }}
  <button @click="update">update</button>
</template>

此时再点击,模板不渲染了,但是耗时操作还是会执行。这是因为processed依赖data,所以data更新后就会重新执行timeConsumingProcessing,但processed没有更改,所以Vue认为不需要更新组件,就跳过了模板渲染。

computed会在其依赖的数据发生任何更改后重新执行,而不关心数据的更改是否会影响结果

这样的优化是失败的,我们的目的应该是阻止timeConsumingProcessing被重复调用,这才是耗时的部分。

使用watch处理数据

<script setup lang="ts">
import { onBeforeUpdate, onUpdated, ref, watch } from 'vue'

const data1 = ref('1')
const data = ref({ key: 'value' })

const update = () => {
  data.value = { key: 'value' }
  data1.value = '1'
}

function timeConsumingProcessing(value: string) {
  console.log('耗时操作正在执行')
  // 模拟耗时操作
  const start = Date.now()
  while(Date.now() - start <= 1000) {
  }
  return value
}

const processed = ref('value')

watch(() => data1.value, () => {
  processed.value = timeConsumingProcessing(data1.value)
}, {
  immediate: true,
})

onBeforeUpdate(() => {
  console.log('组件开始更新')
})

onUpdated(() => {
  console.log('组件已更新')
})
</script>

<template>
  {{ processed }}
  <button @click="update">update</button>
</template>

这个例子中,使用() => data1.value显式地告诉Vue,我需要在data1.value发生改变的时候更新processed

但这并不是一个好的办法,watch是一个风险很高的API,他会导致数据流向难以预测,可读性极差。大量使用watch也很容易出现意外的BUG,而这样的BUG一旦出现,调试会非常麻烦。我的建议是永远不要用watch来做数据的懒处理。

分割组件

创建一个新的组件

  • Field.vue
<script setup lang="ts">
defineProps<{
  value: string
}>()

function timeConsumingProcessing(value: string) {
  console.log('耗时操作正在执行')
  // 模拟耗时操作
  const start = Date.now()
  while(Date.now() - start <= 1000) {
  }
  return value
}
</script>

<template>
  {{ timeConsumingProcessing(value) }}
</template>
  • ReferenceData.vue
<script setup lang="ts">
import { onBeforeUpdate, onUpdated, ref } from 'vue'
import Field from './Field.vue'

const data1 = ref('1')
const data = ref({ key: 'value' })

const update = () => {
  data.value = { key: 'value' }
  data1.value = '1'
}

onBeforeUpdate(() => {
  console.log('组件开始更新')
})

onUpdated(() => {
  console.log('组件已更新')
})
</script>

<template>
  <Field :value="data.key"/>
  <button @click="update">update</button>
</template>

这个例子中,点击update,因为Field组件的Props没有改变,被跳过渲染,所以耗时操作不会被执行。

分割组件在这里扮演的作用类似于computed,但比computed拥有更强大的功能。

在Vue的项目中,大部分的性能问题都是由于在一个组件里塞进去了太多东西,我见过很多项目里的单个vue文件达到几千行,这不但影响可读性,也有可能对性能造成很大影响。

何时分割组件是接下来重点关注的部分,在规范化的文章里我建议在单个文件过大时始终分割组件,但在这里我会会讨论在什么地方分割组件的优化效果最好。

  1. 将不相关的数据分割到不同的组件里。

如果一个组件有两个互不相关的数据,在一个组件里,任何一个数据的修改会触发整个组件的渲染。

将他们分割到独立的组件中,这样当一个组件的数据发生改变,另一个组件不会触发渲染。

2.将v-for的元素分割到单独的组件里。

v-for在性能优化里有着举足轻重的地位,因为v-for依赖的是一个数组或者对象(引用数据类型),这也导致它经常发生不必要的渲染。而且v-for对性能的影响会随着数组的长度直线上升。

很多人对v-for有误解,认为只有发生改变的元素才会重新渲染,而实际上任何一个元素的任何属性发生改变,都将导致整个循环重新渲染。

<script setup lang="ts">
import { ref } from 'vue'

const list = ref([
  {
    data: 1,
  },
  {
    data: 2,
  },
  {
    data: 3,
  },
])

function timeConsumingProcessing(value: number) {
  console.log('耗时操作正在执行')
  // 模拟耗时操作
  const start = Date.now()
  while(Date.now() - start <= 1000) {
  }
  return value
}

const update = () => {
  list.value[0].data++
}
</script>

<template>
  <div>
    <div v-for="item in list">
      {{ timeConsumingProcessing(item.data) }}
    </div>
  </div>
  <button @click="update">
    update
  </button>
</template>

在这个例子中,点击update,timeConsumingProcessing将被调用三次,直到三秒后渲染才会完成。

为了避免不必要的渲染,将v-for的元素提取成为独立的组件很有必要。

  • ForListItem.vue
<script setup lang="ts">
defineProps<{
  data: number
}>()

function timeConsumingProcessing(value: number) {
  console.log('耗时操作正在执行')
  // 模拟耗时操作
  const start = Date.now()
  while(Date.now() - start <= 1000) {
  }
  return value
}
</script>

<template>
{{ timeConsumingProcessing(data) }}
</template>

<style scoped>

</style>
  • ForList.vue
<script setup lang="ts">
import { ref } from 'vue'
import ForListItem from './ForListItem.vue'

const list = ref([
  {
    data: 1,
  },
  {
    data: 2,
  },
  {
    data: 3,
  },
])

const update = () => {
  list.value[0].data++
}
</script>

<template>
  <div>
    <div v-for="item in list">
      <ForListItem :data="item.data"/>
    </div>
  </div>
  <button @click="update">
    update
  </button>
</template>

我知道有很多人在问computed怎么传递参数,实际上是computed根本不能传递参数,也完全不需要。一个组件就有computed的所有功能了,可以说computed就是为“懒人”准备的语法糖,它让开发者可以简单的减少耗时处理方法的调用次数。所以从设计上computed就不能有更复杂的功能,否则他就脱离了存在的初衷。

Vue深度性能优化系列正在更新...

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值