专栏首页Vue中文社区Vue:知道什么时候使用计算属性并不能提高性能吗?

Vue:知道什么时候使用计算属性并不能提高性能吗?

如果你是一个 Vue 用户,你肯定知道计算属性,它用起来很舒服!

个人认为,计算属性是由其他状态(其_依赖项_)组成的状态。但在某些情况下,计算属性也许达不到我们想要的效果,可能很多人都不知道这一点,所以本文将试图解释一下。

当我们在 Vue 中说“计算属性”时,为了清楚我们在谈论什么,这里有一个简单的例子:

const todos = reactive([
  { title: 'Wahs Dishes', done: true},
  { title: 'Throw out trash', done: false }
])

const openTodos = computed(
  () => todos.filter(todo => !todo.done)
)

const hasOpenTodos = computed(
  () => !!openTodos.value.length
)
复制代码

在这里,openTodos依赖于todos, 并且hasOpenTodos依赖于openTodos。这很方便,因为现在我们有了可以传输和使用的响应式对象,并且只要它们依赖的状态发生变化,它们就会自动更新。

如果我们在响应式上下文中使用这些响应式对象,例如 Vue 模板、渲染函数或者一个 watch(),它们也会对计算属性和更新的更改做出反应 - 毕竟这是 Vue 核心的魔法。

注意:我正在使用 composition API,因为这是我最近用的比较多的。不过,本文中描述的行为同样适用于普通 Options API 中的计算属性。毕竟,两者都使用相同的反应系统。

1. 计算属性有什么特别之处

关于计算属性,有两件事使它们变得特别,并且它们与本文的要点相关:

  1. 它们的结果会被缓存,并且只需要在其反应性依赖项之一发生变化时重新计算。
  2. 它们在访问时被惰性计算。

缓存

计算属性的结果被缓存。在我们上面的例子中,这意味着只要todos数组没有改变,openTodos.value多次调用将返回相同的值,而无需重新运行 filter 方法。这对于很耗性能的任务尤其有用。

懒惰评估

计算属性也会被_惰性_计算——但这究竟意味着什么?

这意味着计算属性的回调函数只会在计算值被读取时运行(最初或在它被标记为更新之后,因为它的依赖项之一发生了变化)。

因此,如果任何东西都没有使用具有很耗性能计算的计算属性,那么该很耗性能的操作甚至不会首先完成 - 在大量数据上进行繁重工作时的另一个性能优势。

2. 当惰性求值可以_提高_性能时

如前一段所述,计算属性的延迟评估通常是一件好事,尤其是对于很耗性能的操作:它确保仅在实际需要结果时才进行评估。

这意味着如果那个时候你的代码的任何部分都不会读取和使用过滤的结果,那么过滤大列表之类的事情将被简单地跳过。这是一个示例:

<template>
  <input type="text" v-model="newTodo">
  <button type="button" v-on:click="addTodo">Save</button>
  <button @click="showList = !showList">
    Toggle ListView
  </button>
  <template v-if="showList">
    <template v-if="hasOpenTodos">
      <h2>{{ openTodos.length }} Todos:</h2> 
      <ul>
        <li v-for="todo in openTodos">
          {{ todo.title }}
        </li>
      </ul>
    </template>
    <span v-else>No todos yet. Add one!</span>
  </template>
</template>

<script setup>
const showListView = ref(false)

const todos = reactive([
  { title: 'Wahs Dishes', done: true},
  { title: 'Throw out trash', done: false }
])
const openTodos = computed(
  () => todos.filter(todo => !todo.done)
)
const hasOpenTodos = computed(
  () => !!openTodos.value.length
)

const newTodo = ref('')
function addTodo() {
  todos.push({
    title: todo.value,
    done: false
  })
}
</script>
复制代码

请参阅此代码在SFC Playground[1]上运行[2]

由于showList最初是false,模板/渲染函数不会读取openTodos,因此,过滤甚至不会发生,无论是最初还是在添加新的待办事项并todos.length发生更改之后。只有在showList设置为 之后true,才会读取这些计算属性并触发它们的计算。

当然,在这个小例子中,过滤的工作量是最小的,但你可以想象,对于更耗性能的操作,这可能是一个巨大的好处。

3. 当惰性求值会_降低_性能时

这有一个缺点:如果计算属性返回的结果只能在您的代码在某处使用它之后才能知道,这也意味着 Vue 的 Reactivity 系统无法事先知道这个返回值。

换句话说,Vue 可以意识到计算属性的一个或多个依赖项发生了变化,因此应该在下次读取时重新计算它,但此时 Vue 无法知道返回的_结果_是否为计算的属性实际上会有所不同。

为什么这会成为问题?

代码的其他部分可能取决于该计算属性——可能是另一个计算属性,可能是一个 watch(),可能是模板/渲染函数。

所以 Vue 别无选择,只能将这些依赖项也标记为更新——“以防万一”返回值会有所不同。

如果这些是很耗性能的操作,即使您的计算属性返回与以前相同的值,您也可能触发了耗性能的重新计算,因此这里是没必要重新计算的。

证明问题

这是一个简单的示例:假设我们有一个项目列表和一个用于增加计数器的按钮。一旦计数器达到 100,我们想以相反的顺序显示列表(是的,这个例子很愚蠢。干它)。

(你可以在这个SFC playground[3]上玩这个例子)\

<template>
  <button @click="increase">
    Click me
  </button>
  <br>
  <h3>
    List
  </h3>
  <ul>
    <li v-for="item in sortedList">
      {{ item }}
    </li>
  </ul>
</template>

<script setup>
import { ref, reactive, computed, onUpdated } from 'vue'

const list = reactive([1,2,3,4,5])

const count = ref(0)
function increase() {
  count.value++
}

const isOver100 = computed(() => count.value > 100)

const sortedList = computed(() => {
  // imagine this to be expensive
  return isOver100.value ? [...list].reverse() : [...list]
})

onUpdated(() => {
  // this eill log whenever the component re-renders
  console.log('component re-rendered!')
})
</script>
复制代码

问题:您单击按钮 101 次。我们的组件多久重新渲染一次?

得到你的答案了吗?你确定?

答: 它将重新渲染101 次

我怀疑你们中的一些人可能期望得到不同的答案,例如:“一次,在第 101 次点击时”。但这是错误的,其原因是计算属性的惰性计算。

有点困惑?我们逐步分析一下正在发生的事情:

  1. 当我们点击按钮时,count增加了。组件不会重新渲染,因为我们没有在模板中使用计数器。
  2. 但是自从count改变后,我们的计算属性isOver100被标记为“dirty”——一个响应式依赖改变了,所以它的返回值必须重新计算。
  3. 但是由于惰性计算,这只会在其他内容读取isOver100.value时发生 - 在此之前,我们(和 Vue)不知道此计算属性是否仍会返回false或将更改为true.
  4. sortedList取决于isOver100- 所以它也必须被标记为“dirty”。同样,它还不会被重新计算,因为这只会在被读取时发生。
  5. 由于我们的模板依赖于sortedList,并且它被标记为“dirty”(可能已更改,需要重新计算),因此组件将重新渲染。
  6. 在渲染过程中,它读取 sortedList.value
  7. sortedList现在重新计算,并读取isOver100.value- 现在重新计算,但仍会false再次返回。
  8. 所以现在我们重新渲染了组件_并_重新运行了“很耗性能的”sorteList计算,即使所有这些都是不必要的 - 生成的新虚拟 DOM / 模板看起来完全一样。

真正的罪魁祸首是isOver100——它是一个经常更新的计算,但通常返回与以前相同的值,而且最重要的是,它是一个廉价的操作,并没有真正从缓存计算属性中获益。我们只是使用了计算机,因为它感觉符合人体工程学,它“很好”。

当在另一个耗性能的计算(它从缓存_中_受益)或模板中使用时,它会触发不必要的更新,这会根据场景严重降低代码的性能。

本质上是这样的组合:

  1. 一个耗性能的计算属性、观察者或模板取决于
  2. 另一个经常重新计算为相同值的计算属性。

4. 当你遇到这个问题时如何解决它

现在你可能有两个问题:

  1. 哇!这是一个问题吗?
  2. 我该如何摆脱它?

所以首先:冷静。通常,这不是什么大问题。Vue 的反应系统通常非常高效,重新渲染也是如此,尤其是现在在 Vue 3 中。通常,这里和那里的一些不必要的更新仍然会比默认情况下重新渲染_任何状态_的 React 对应物表现得更好_随便改_。

因此,该问题仅适用于在一个地方混合了频繁状态更新的特定场景,这会在另一个耗性能的地方(非常大的组件、计算量很大的计算属性等)触发频繁的不必要更新。

如果你遇到这样的情况,幸运的是你有不同的解决方法:

  1. 使用普通函数而不是独立的计算属性
  2. 在对象上使用 Getter 而不是计算属性
  3. 使用自定义的 "eagerly computed" 属性

普通函数

如果我们的计算属性的操作是一个廉价的单线操作,我们可以使用一个函数来代替:

// computed 写法
const hasOpenTodos = computed(() => !!openTodos.value.length)
// usage
if (hasOpenTodos.value) {
  // list open todos
}

// 普通函数写法
const hasOpenTodos = () => !!openTodos.value.length
// Usage
if (hasOpenTodos()) {
  // list open todos
}
复制代码

两种方式都提供了描述性命名,但第二种方式可能对整体性能更好一点,因为一个简单的函数在内存和 CPU 使用率上比计算属性更轻,而且它的操作——读取数组的长度——非常便宜计算的缓存行为不会为此提供任何好处。

一个简单的函数不会有惰性求值,所以我们不会冒险触发模板/渲染函数、观察者或其他计算属性的不必要的效果运行。

现在,在大多数情况下,这可能不会产生很大的影响,但在某些情况下,它可能会产生影响。想象一下,一个组件使用了几个这种计算属性,_并且_在一个大列表中被多次渲染——在这里,使用函数而不是计算属性肯定可以节省一些内存。

我想说,在几乎所有情况下,单独使用计算属性仍然可以。如果你更喜欢计算属性的风格而不是简单的函数,那么就做你喜欢的。

Getters

我还看到过这样一种使用方式:

import { reactive, computed } from 'vue'
const state = reactive({
  name: 'Linusborg',
  bigName: computed(() => state.name.toUpperCase())
})
复制代码

如果您想要一个对象的某些属性从其他属性派生出来,这会很方便。

但实际上,在这个例子中,计算属性是多余的。Javascript 有自己的方法来为对象属性派生状态 - 称为Getters[4]。它没有缓存或惰性计算,但在这里刚好合适。

import { reactive } from 'vue'
const state = reactive({
  name: 'Linusborg',
  get bigName() { return state.name.toUpperCase() )
})
复制代码

问题解决了。

自定义eagerComputed助手

普通函数和 getter 很好,但对于我们这些习惯了 Vue 做事方式的人来说,计算属性可能会感觉更好。幸运的是,Vue 的的响应式系统为我们提供了所需的所有工具来构建我们自己的版本的 computed(),一个用于计算_急切_,不_惰性_的情况。

让我们称之为 eagerComputed()

import { watchEffect, shallowRef, readonly } from 'vue'
export function eagerComputed(fn) {
  const result = shallowRef()
  watchEffect(() => {
    result.value = fn()
  }, 
  {
    flush: 'sync' // needed so updates are immediate.
  })

  return readonly(result)
}
复制代码

然后我们可以像使用计算属性一样使用它,但行为的不同在于更新将是急切的,而不是惰性的,摆脱不必要的更新。

查看此 SFC Playground[5]上的固定示例[6]

你什么时候用computed(),什么时候用eagerComputed()

  • computed()当您正在进行复杂的计算时使用,这实际上可以从缓存和延迟计算中受益,并且应该只在真正必要时(重新)计算。
  • 使用eagerComputed()时,你有一个简单的操作,用很少改变返回值-通常是一个布尔值。

注意:请记住,这仍然会增加一些开销,因为它使用了一堆响应式 API - 在_非常_敏感的场景中,一个简单的函数通常会更有效。

参考文章:dev.to/linusborg/v…[7]

感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞

作者:gyx_这个杀手不太冷静 https://juejin.cn/post/7005336858049642527

推荐阅读:
使用 Performance 看看浏览器在做些什么从 ESLint 开始,说透我如何在团队项目中基于 Vue 做代码校验
史上最全 Vue 前端代码风格指南
2021, 九款值得推荐的VUE3 UI框架
推荐 130 个令你眼前一亮的网站,总有一个用得着深入浅出 33 道 Vue 99% 出镜率的面试题
VUE中文社区 编程技巧 · 行业秘闻 · 技术动向
文章分享自微信公众号:
Vue中文社区

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!

原始发表时间:2021-10-22
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • 前端面试题最新

    1.vue的原理? 2.v-model双向绑定的原理? 3.全局导航钩子函数应用场景? 4.路由独享的守卫(路由内钩子) 5.请说出XHTML和HTML...

    酷走天涯
  • 这些Vue知识点,解决你的卡点

    在开发Vue的过程中,我们经常会遇到一些这样那样的问题,然后要卡好半天,等问题解决了才发现原来一些细节知识点还是没有掌握好。今天小编就整理了几个在项目中会用到的...

    用户1308196
  • vue必会面试题+答案

    React是pull的方式侦测变化,当React知道发生变化后,会使用Virtual Dom Diff进行差异检测,但是很多组件实际上是肯定不会发生变化的,...

    zz1998
  • 【Web技术】847- Virtual DOM 认知误区

    链接:https://juejin.cn/post/6898526276529684493

    pingan8787
  • 化身面试官出 30+ Vue 面试题,超级干货(附答案)

    不知道大伙是不是已经在准备春招面试了呢,准备得咋样了呢,面试的 Vue 复习得怎么样了呢?如果你感觉在 vue 这方面还比较薄弱的话,不如来做一做这套模拟面试吧...

    网罗开发
  • 一年双非本科的大厂面试经历

    如findLastIndex([1,2,3,3,3,4,5], 3), 返回4。时间复杂度是多少?什么情况下时间复杂度最高?2. 请实现一个cacheReque...

    落落落洛克
  • vue项目你一定会用到的性能优化!

    而本渣最近维护的项目恰巧在这个方向下了很大功夫,一些经验之谈奉上,希望对大家有些许帮助!

    用户7413032
  • 大厂的面试题

    Qiang
  • 2019前端工程师自检清单与思考

    对于JavaScript,掌握其语法和特性是最基本的,但是这些只是应用能力,最终仍旧考量仍然是计算机体系的理论知识,所以数据结构,算法,软件工程,设计模式等基础...

    super.x
  • 2019 前端工程师自检清单与思考

    对于 JavaScript,掌握其语法和特性是最基本的,但是这些只是应用能力,最终仍旧考量仍然是计算机体系的理论知识,所以数据结构,算法,软件工程,设计模式等基...

    桃翁
  • Vuex 映射完全指南

    Vuex 是一把双刃剑。如果使用得当,Vue 可以使你的工作更加轻松。如果不小心,也会使让的代码混乱不堪。

    疯狂的技术宅
  • 巧用滑动选项卡,提升用户体验

    目前针对移动设备的Cordova应用程序和渐进式的Web应用程序非常流行。提升用户体验和交互的关键是传递出原生的视觉效果和感觉,这并不总是一件容易的事情。当然,...

    疯狂的技术宅
  • 2022必会的vue高频面试题(附答案)

    由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。

    helloworld1024
  • 2021大厂(阿里、百度、字体跳动、腾讯)前端面试题库

    ​​html和css部分 1.如何理解CSS盒子模型 2.BFC 3.标签语义化? 4.css与javascript引入设置 5.如何理解CSS盒子模型 6.H...

    用户8644488

扫码关注腾讯云开发者

领取腾讯云代金券