Vue.js 计算属性不更新

83
我正在使用Vue.js的计算属性,但遇到了一个问题:计算方法在正确的时间被调用,但计算方法返回的值被忽略了!
我的方法是:
computed: {
  filteredClasses() {
    let classes = this.project.classes
    const ret = classes && classes.map(klass => {
      const klassRet = Object.assign({}, klass)
      klassRet.methods = klass.methods.filter(meth => this.isFiltered(meth, klass))
      return klassRet
    })
    console.log(JSON.stringify(ret))
    return ret
  }
}

console.log语句打印出的值是正确的,但是当我在模板中使用filteredClasses时,它只使用第一个缓存的值,并且从未更新模板。这已经通过Vue chrome devtools确认(filteredClasses在初始缓存后从未更改)。

有人能告诉我为什么会发生这种情况吗?

Project.vue

<template>
  <div>
    <div class="card light-blue white-text">
      <div class="card-content row">
        <div class="col s4 input-field-white inline">
          <input type="text" v-model="filter.name" id="filter-name" />
          <label for="filter-name">Name</label>
        </div>
        <div class="col s2 input-field-white inline">
          <input type="text" v-model="filter.status" id="filter-status" />
          <label for="filter-status">Status (PASS or FAIL)</label>
        </div>
        <div class="col s2 input-field-white inline">
          <input
            type="text"
            v-model="filter.apkVersion"
            id="filter-apkVersion"
          />
          <label for="filter-apkVersion">APK Version</label>
        </div>
        <div class="col s4 input-field-white inline">
          <input
            type="text"
            v-model="filter.executionStatus"
            id="filter-executionStatus"
          />
          <label for="filter-executionStatus"
            >Execution Status (RUNNING, QUEUED, or IDLE)</label
          >
        </div>
      </div>
    </div>
    <div v-for="(klass, classIndex) in filteredClasses">
      <ClassView :klass-raw="klass" />
    </div>
  </div>
</template>

<script>
import ClassView from './ClassView.vue'

export default {
  name: 'ProjectView',

  props: {
    projectId: {
      type: String,
      default () {
        return this.$route.params.id
      }
    }
  },

  data () {
    return {
      project: {},
      filter: {
        name: '',
        status: '',
        apkVersion: '',
        executionStatus: ''
      }
    }
  },

  async created () {
    // Get initial data
    const res = await this.$lokka.query(`{
            project(id: "${this.projectId}") {
                name
                classes {
                    name
                    methods {
                        id
                        name
                        reports
                        executionStatus
                    }
                }
            }
        }`)

    // Augment this data with latestReport and expanded
    const reportPromises = []
    const reportMeta = []
    for (let i = 0; i < res.project.classes.length; ++i) {
      const klass = res.project.classes[i]
      for (let j = 0; j < klass.methods.length; ++j) {
        res.project.classes[i].methods[j].expanded = false
        const meth = klass.methods[j]
        if (meth.reports && meth.reports.length) {
          reportPromises.push(
            this.$lokka
              .query(
                `{
                           report(id: "${
                             meth.reports[meth.reports.length - 1]
                           }") {
                               id
                               status
                               apkVersion
                               steps {
                                   status platform message time
                               }
                           }
                       }`
              )
              .then(res => res.report)
          )
          reportMeta.push({
            classIndex: i,
            methodIndex: j
          })
        }
      }
    }

    // Send all report requests in parallel
    const reports = await Promise.all(reportPromises)

    for (let i = 0; i < reports.length; ++i) {
      const { classIndex, methodIndex } = reportMeta[i]
      res.project.classes[classIndex].methods[methodIndex].latestReport =
        reports[i]
    }

    this.project = res.project

    // Establish WebSocket connection and set up event handlers
    this.registerExecutorSocket()
  },

  computed: {
    filteredClasses () {
      let classes = this.project.classes
      const ret =
        classes &&
        classes.map(klass => {
          const klassRet = Object.assign({}, klass)
          klassRet.methods = klass.methods.filter(meth =>
            this.isFiltered(meth, klass)
          )
          return klassRet
        })
      console.log(JSON.stringify(ret))
      return ret
    }
  },

  methods: {
    isFiltered (method, klass) {
      const nameFilter = this.testFilter(
        this.filter.name,
        klass.name + '.' + method.name
      )
      const statusFilter = this.testFilter(
        this.filter.status,
        method.latestReport && method.latestReport.status
      )
      const apkVersionFilter = this.testFilter(
        this.filter.apkVersion,
        method.latestReport && method.latestReport.apkVersion
      )
      const executionStatusFilter = this.testFilter(
        this.filter.executionStatus,
        method.executionStatus
      )
      return (
        nameFilter && statusFilter && apkVersionFilter && executionStatusFilter
      )
    },
    testFilter (filter, item) {
      item = item || ''
      let outerRet =
        !filter ||
        // Split on '&' operator
        filter
          .toLowerCase()
          .split('&')
          .map(x => x.trim())
          .map(seg =>
            // Split on '|' operator
            seg
              .split('|')
              .map(x => x.trim())
              .map(segment => {
                let quoted = false,
                  postOp = x => x
                // Check for negation
                if (segment.indexOf('!') === 0) {
                  if (segment.length > 1) {
                    segment = segment.slice(1, segment.length)
                    postOp = x => !x
                  }
                }
                // Check for quoted
                if (segment.indexOf("'") === 0 || segment.indexOf('"') === 0) {
                  if (segment[segment.length - 1] === segment[0]) {
                    segment = segment.slice(1, segment.length - 1)
                    quoted = true
                  }
                }
                if (!quoted || segment !== '') {
                  //console.log(`Item: ${item}, Segment: ${segment}`)
                  //console.log(`Result: ${item.toLowerCase().includes(segment)}`)
                  //console.log(`Result': ${postOp(item.toLowerCase().includes(segment))}`)
                }
                let innerRet =
                  quoted && segment === ''
                    ? postOp(!item)
                    : postOp(item.toLowerCase().includes(segment))

                //console.log(`InnerRet(${filter}, ${item}): ${innerRet}`)

                return innerRet
              })
              .reduce((x, y) => x || y, false)
          )
          .reduce((x, y) => x && y, true)

      //console.log(`OuterRet(${filter}, ${item}): ${outerRet}`)
      return outerRet
    },
    execute (methID, klassI, methI) {
      this.project.classes[klassI].methods[methI].executionStatus = 'QUEUED'
      // Make HTTP request to execute method
      this.$http.post('/api/Method/' + methID + '/Execute').then(
        response => {},
        error => console.log("Couldn't execute Test: " + JSON.stringify(error))
      )
    },
    registerExecutorSocket () {
      const socket = new WebSocket('ws://localhost:4567/api/Executor/')

      socket.onmessage = msg => {
        const { methodID, report, executionStatus } = JSON.parse(msg.data)

        for (let i = 0; i < this.project.classes.length; ++i) {
          const klass = this.project.classes[i]
          for (let j = 0; j < klass.methods.length; ++j) {
            const meth = klass.methods[j]
            if (meth.id === methodID) {
              if (report)
                this.project.classes[i].methods[j].latestReport = report
              if (executionStatus)
                this.project.classes[i].methods[j].executionStatus =
                  executionStatus
              return
            }
          }
        }
      }
    },
    prettyName: function (name) {
      const split = name.split('.')
      return split[split.length - 1]
    }
  },

  components: {
    ClassView: ClassView
  }
}
</script>

<style scoped></style>

只是为了澄清,每当project.classes数据发生更改时,方法filteredClasses都会运行,但返回值ret没有更新? - Adam
@Adam,本地变量ret正在正确修改。Vue只是没有获取该值并更新vm.computed.filteredClasses - Jared Loomis
非常奇怪的是 console.log(JSON.stringify(ret)) 显示了正确的值,但是 return ret 却出现了问题。肯定还有其他问题,没有理由会出现这样的问题。当你说 "filteredClasses 在初始缓存后从未更改" 时,你具体指的是什么?在用户界面上吗?最后,你确定你没有一个名为 filteredClasses 的方法或数据属性吗? - Adam
这里有一个 JSFiddle,展示了你的一般方法应该是可行的:https://jsfiddle.net/v0673trh/ - Adam
一个展示问题的CodePen或Fiddle会在这里起到奇效。 - Bert
@BertEvans 这是有问题的组件的完整源代码 http://pastebin.com/C8Yxbu0f - 正在努力将其提炼为可运行的小样例。 - Jared Loomis
9个回答

97
如果你想让计算属性在project.classes.someSubProperty更改时更新,那么在定义计算属性时,该子属性必须存在。Vue无法检测属性的添加或删除,只能检测现有属性的更改。
当我使用一个空的state对象与Vuex store一起使用时,这个问题困扰了我。我的状态后续更改不会导致依赖于它的计算属性被重新评估。在Veux状态中添加具有空值的显式键解决了这个问题。
我不确定在你的情况下是否可行,但它可能有助于解释为什么计算属性变得过时。
有关更多信息,请参阅Vue反应性文档: https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats

9
如上述链接中所示,您可以使用this.$set(this.someObject, 'b', 2) - nick
@Nick 或许我错了,但我不明白 this.$set(this.someObject, 'b', 2) 如何有所帮助:如果属性 'b' 最初没有被设置,那么无论是计算属性还是观察属性都无法帮助,因为它们仍然不会做出反应。 - rubebop
1
如在所引用的文档中所述:“但是,可以使用Vue.set(object, propertyName, value)方法向嵌套对象添加反应式属性: Vue.set(vm.someObject,'b',2)”。只有根级别属性不能被添加。 - nick
天啊...我也被这个问题困扰过。我一直在处理Vue的计算属性,例如使用方法和观察逻辑.. 这是一个非常合理的解决方案,只需在Veux上创建空的prop即可。谢谢! - Eagle_

35

我之前遇到过类似的问题,并通过使用普通方法而不是计算属性来解决它。只需将所有内容移到一个方法中,并返回您的ret即可。 官方文档。


13
这个链接描述了计算机属性和方法之间的区别。使用方法可能可以解决这个问题,但并不是理想的解决方案,因为方法不使用缓存。 - Wilson Freitas
13
谢谢!我也使用过 this.$forceUpdate() - Ehvince
1
@WilsonFreitas 我想象中这个问题的根源是缓存,正如问题中所提到的。 - Abraham Brookes
2
@AbrahamBrookes 我认为OP的问题与他更新组件状态的方式有关。Vue无法检测到他的更改,因此组件不会对其做出反应。如果状态根据Vue响应性指南进行更新,则缓存不应该成为问题。 - Wilson Freitas
@WilsonFreitas 在解决类似问题时,经过一些尝试,我认为你是正确的。当 OP 使用 Object.assign() 时,他们正在向对象添加非跟踪属性。我认为我可以回答这个问题。 - Abraham Brookes
显示剩余2条评论

14
我曾遇到这个问题:当值为undefined时,计算属性无法检测到变化。我通过给它一个空的初始值来解决了这个问题。 根据Vue文档:

我曾遇到这个问题:当值为undefined时,计算属性无法检测到变化。我通过给它一个空的初始值来解决了这个问题。

根据Vue文档

enter image description here


5
这是一个关于数据变量无法响应式的正确解释,但不适用于计算属性,这也是这个问题所涉及的内容。 - GiovanH

7

注意:除了这种解决方法,还可以看下面我的更新。

我有一个解决这种情况的方法,不知道你是否喜欢。我在data()下放置一个整数属性(我们称之为trigger),每次计算属性中使用的对象发生更改时,trigger会自增1。以这种方式,计算属性会在对象更改时更新。

例如:

export default {
data() {
  return {
    trigger: 0, // this will increment by 1 every time obj changes
    obj: { x: 1 }, // the object used in computed property
  };
},
computed: {
  objComputed() {
    // do anything with this.trigger. I'll log it to the console, just to be using it
    console.log(this.trigger);

    // thanks to this.trigger being used above, this line will work
    return this.obj.y;
  },
},
methods: {
  updateObj() {
    this.trigger += 1;
    this.obj.y = true;
  },
},
};

更新:在官方文档中找到了更好的方法,因此您不需要像this.trigger这样的东西。

使用this.$set()的相同示例:

export default {
data() {
  return {
    obj: { x: 1 }, // the object used in computed property
  };
},
computed: {
  objComputed() {
    // note that `y` is not a property of `this.obj` initially
    return this.obj.y;
  },
},
methods: {
  updateObj() {
    // now the change will be detected
    this.$set(this.obj, 'y', true);
  },
},
};

这里有一个链接可用


如果对象从组件外部被更改了,那该怎么办? - niko
你的意思是通过在组件上使用 ref 来更改对象吗?那么,你可以像更改对象一样更改 trigger 的值。 - Ridvan Sumset
谢谢这个。我遇到了一个问题,一个计算属性依赖于加载图像来计算其宽高比。在我的计算方法中添加一个关于图像是否加载的引用解决了这个问题。基本上是这样的:if (this.loading) { return -1; } else { ... } - undefined

3
您需要在v-for的列表项中分配一个唯一的键值。像这样...
<ClassView :klass-raw="klass" :key="klass.id"/>

否则,Vue 不知道应该更新哪些项目。详细解释请参见https://v2.vuejs.org/v2/guide/list.html#key

当属性被更新时,即使组件未在v-for中使用,我也曾经用这种方法解决了我的问题。 - Icode4food

3
因为我通过这种方式改变了数组:arrayA[0] = value,所以对象没有被响应。虽然arrayA已经改变,但是计算属性从arrayA计算的值并未触发。您需要使用$set而不是给arrayA[0]赋值,例如。更深入地了解可以阅读下面链接: https://v2.vuejs.org/v2/guide/reactivity.html 我还使用了一些技巧,如在计算属性中添加了缓存标记cache=false
compouted: {
   data1: {
      get: () => {
         return data.arrayA[0]
      },
      cache: false
   }
}

3

如果在返回之前添加console.log,您可以在filteredClasses中看到计算出的值。

但因某些原因,DOM不会更新。

然后,您需要强制重新呈现DOM。

重新渲染的最佳方法只是将键添加为计算值,如下所示。

<div
  :key="JSON.stringify(filteredClasses)" 
  v-for="(klass, classIndex) in filteredClasses"
>
  <ClassView
    :key="classIndex"
    :klass-raw="klass"
  />
</div>

注意

不要使用非原始值(如对象和数组)作为键。请使用字符串或数值。

这就是我将数组filteredClasses转换为字符串的原因。(也可以使用其他数组转换方法)

同时,我也想说“尽可能在v-for中提供一个key属性是推荐的。”


1

如果其他人在Vue3上遇到了同样的问题,我刚刚解决了它,并且通过使用提供的ref()函数将从setup()函数返回并需要响应式的值包装在引用中,成功摆脱了以前需要的所有this.$forceUpdate(),如下所示:

import { defineComponent, ref } from 'vue'

export default defineComponent({
  name: 'CardDisplay',
  props: {
    items: {
      type: Array,
      default: () => []
    },
    itemComponent: Object,
    maxItemWidth: { type: Number, default: 200 },
    itemRatio: { type: Number, default: 1.25 },
    gapSize: { type: Number, default: 50 },
    maxYCount: { type: Number, default: Infinity }
  },
  setup () {
    return {
      containerSize: ref({ width: 0, height: 0 }),
      count: ref({ x: 0, y: 0 }),
      scale: ref(0),
      prevScrollTimestamp: 0,
      scroll: ref(0),
      isTouched: ref(false),
      touchStartX: ref(0),
      touchCurrentX: ref(0)
    }
  },
  computed: {
    touchDeltaX (): number {
      return this.touchCurrentX - this.touchStartX
    }
  },
  ...
}

这样做后,对封装值的每个更改都会立即反映出来!


0
如果您在Vue已经注册对象以进行响应性之后向返回的对象添加属性,则当这些新属性发生更改时,Vue将不知道要监听它们。以下是类似的问题:
let classes = [
    {
        my_prop: 'hello'
    },
    {
        my_prop: 'hello again'
    },
]

如果我将这个数组加载到我的vue实例中,vue将把这些属性添加到其反应性系统中,并能够监听它们的变化。然而,如果我从计算函数内部添加新属性:
computed: {
    computed_classes: {
        classes.map( entry => entry.new_prop = some_value )
    }
}

对于new_prop的任何更改都不会导致vue重新计算属性,因为我们实际上从未将classes.new_prop添加到vue的响应系统中。

回答你的问题,你需要在将对象传递给vue之前构建具有所有反应性属性的对象 - 即使它们只是null。任何与vues响应系统有困难的人都应该阅读此链接:https://v2.vuejs.org/v2/guide/reactivity.html


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接