如何准确的使用响应式数据,让你的程序性能更上一层台阶

299 阅读4分钟

使用 vue3 的一些注意事项,让你系统性能有飞一样的提升

vue3 是一个是非优秀的框架,他的心智模型更加贴切 js 本身。并且官方就做了尽可能多的优化,让你不需要像写 react 那样,需要去考虑很多性能问题。

但是,vue3 也有一些需要注意的地方,这些地方如果处理不当,可能会导致系统性能下降。特别是我们在自定义 hooks 与封装 vue3 组件的时候,需要注意到这一点。

哪些老生常谈的点,就不再赘述了,这里只说一些容易被忽视的点。并且我们预设使用 Naive UI 作为组件库举栗子。

无意义的响应式数据

何为无意义的响应式数据?也就是说说该数据不会影响视图上的任何内容。

我们来看一些栗子:

有意义的响应式数据

<template>
  <div>
    <div>{{ count }}</div>
    <button @click="countBtnClick">Increment</button>
  </div>
</template>
<script setup>
const count = ref(0)

const countBtnClick = () => {
  count.value++
}
</script>

上面这段代码是很典型的响应式数据代码。每当 count 发生变化时,vue 会重新渲染视图。

无意义的响应式数据

<template>
  <NDataTable :columns="columns" :data="data" />
</template>
<script setup>
import { NDataTable } from 'naive-ui'

const columns = ref([
  {
    title: 'Name',
    key: 'name',
  },
  {
    title: 'Age',
    key: 'age',
  },
])
const data = ref([])

for (let i = 0; i < 10; i++) {
  data.value.push({
    name: 'John',
    age: 20,
  })
}
</script>

我们来观察这个代码的特点:

  1. 声明了一个 NDataTable 组件,并且传入了 columnsdata 两个属性。
  2. 动态的更新了 data 的值。

有没有发现,columns 是无意义的响应式声明。也就是说,columns 是不会发生变化的,所以我们不需要使用 ref 来声明 columns

而是把代码改为这样:

const columns = [ { title: 'Name', key: 'name', }, ]

当然了,这是一个很不起眼的栗子。并且有些人可能会疑问,就这么点数据,性能能差多少?

你说得对,但是,如果数据量很大呢?

声明了大量无意义的响应式数据

假设我们需要给 高德地图 注入海量的点位数据,那么我们可能会这样做:

// 假设这是一个用来注入海量点位的自定义 hooks
export const useMassMarks = () => {
  const massMarkersInstance = ref(null)

  const setMassMarks = (mk) => {
    const p = mk.map((curr) => {
      // 省略一些代码...
    })

    massMarkersInstance.value = new AMap.MassMarks(p, {
      // 省略一些代码...
    })
  }

  const getMassMarksInstance = () => {
    return massMarkersInstance.value
  }

  const getMassMarks = () => {
    return massMarks.value
  }

  return { setMassMarks, getMassMarksInstance, getMassMarks }
}
<script setup>
import { useMassMarks } from './useMassMarks'

const { setMassMarks, getMassMarksInstance, getMassMarks } = useMassMarks()
const p = []

for (let i = 0; i < 1000000; i++) {
  p.push({
    id: i,
    position: [116.397428, 39.90923],
  })
}

setMassMarks(p)
</script>

乍一看,这段代码好像没什么问题。但是:massMarksInstance 被响应式代理了 1000000 条数据,并且 massMarks 也被响应式代理了。

会导致整个渲染过程十分的卡顿。

这时候会有人说,可以用 shallowRef 来声明数据。对,你说的确实没问题。但是这个就是 栗子一中 提到的 无意义响应式数据

当我们在做自定义 hooks 的时候,需要注意到这一点。如何避免无意义的响应式数据的声明。如果需要获取到一个数据,我们应该提供一个 get 方法来获取。

我们把 useMassMarks 改成这样:

export const useMassMarks = () => {
  let massMarkersInstance = null

  const setMassMarks = (mk) => {
    const p = mk.map((curr) => {
      // 省略一些代码...
    })

    massMarkersInstance = new AMap.MassMarks(p, {
      // 省略一些代码...
    })
  }

  const getMassMarksInstance = () => {
    return massMarkersInstance
  }

  return { setMassMarks, getMassMarksInstance }
}

这样,massMarkersInstance 就不会被响应式代理了。并且整个地图的渲染过程会十分的流畅。

自定义组件

假设我们使用 interact.js 定义一个 Draggable 组件,并且传入一个 props 属性。

<template>
  <div ref="draggableRef"></div>
</template>
<script setup>
import interact from 'interactjs'

const draggableRef = useTemplateRef('draggableRef')
const inst = shallowRef(null)
const expose = defineExpose()

expose()

onMounted(() => {
  inst.value = interact(draggableRef.value).draggable({
    // 省略实现代码...
  })
})
onBeforeUnmount(() => {
  inst.value?.destroy()
  inst.value = null
})
</script>

是不是感觉这个代码没啥大毛病?但是,一个 Draggable 组件,并不需要响应式的 inst 实例。

这时候有人会说,你这里 expose 暴露了一个空对象出去,如果我需要拿到这个 inst 实例,我需要怎么办?

那么我们可以通过实现 get 方法来获取到 inst 实例,而不是靠响应式数据去获取。

<template>
  <div ref="draggableRef"></div>
</template>
<script setup>
import interact from 'interactjs'

const draggableRef = useTemplateRef('draggableRef')
let inst = null
const expose = defineExpose()

const getInst = () => {
  return inst
}

expose({ getInst })

onMounted(() => {
  inst = interact(draggableRef.value).draggable({
    // 省略实现代码...
  })
})
onBeforeUnmount(() => {
  inst?.destroy()
  inst = null
})
</script>

总结

为什么需要去举例子这些?

因为本人最近在写地图相关的一些业务,在封装一些 hooks, components 的时候,一不小心踩到了这些坑的问题。

特别是海量点的相关 components, hooks,为啥我做出来的东西这么卡顿。

也是自己的技术水平不足,遇到了这些本不应该有的问题。并且在 vue3 官方也准确的提出了这些点,所以还是要多看文档才行(手动尴尬)。