【问题标题】:Vue.js computed property not updatingVue.js 计算属性未更新
【发布时间】:2017-07-29 11:20:21
【问题描述】:

我正在使用 Vue.js 计算属性,但遇到了一个问题:计算方法 IS 在正确的时间被调用,但计算方法返回的值被忽略了!

我的方法

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 在初始缓存后永远不会改变)。

谁能给我一些关于为什么会发生这种情况的信息?

项目.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 数据发生变化但返回值ret 没有更新时,都会运行filteredClasses 方法?
  • @Adam 局部变量 ret 正在被正确修改。 Vue 只是没有采用该值并更新 vm.computed.filteredClasses
  • 很奇怪console.log(JSON.stringify(ret)) 显示正确的值但return ret 已损坏。必须有其他事情发生,没有理由应该有问题。当你说“filteredClasses 在初始缓存后永远不会改变”。你到底什么意思?在用户界面中?最后,您确定没有也称为 filteredClasses 的方法或数据属性吗?
  • 这里有一个小提琴,表明您的一般方法应该有效:jsfiddle.net/v0673trh
  • 一个代码笔或小提琴证明这个问题会在这里创造奇迹。

标签: javascript vue.js single-page-application


【解决方案1】:

如果您的意图是在 project.classes.someSubProperty 更改时更新计算属性,则在定义计算属性时该子属性必须存在。 Vue 无法检测到属性的添加或删除,只能对现有属性进行更改。

当使用带有 en 空 state 对象的 Vuex 商店时,这让我很痛苦。我对状态的后续更改不会导致依赖于它的计算属性被重新评估。将具有空值的显式键添加到 Veux 状态解决了这个问题。

我不确定显式键在您的情况下是否可行,但它可能有助于解释为什么计算的属性会过时。

Vue reactiviy 文档,了解更多信息: https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats

【讨论】:

  • 正如上面链接的文档所建议的,您可以使用this.$set(this.someObject, 'b', 2)
【解决方案2】:

我之前遇到过类似的问题,并通过使用常规方法而不是计算属性来解决它。只需将所有内容移入一个方法并返回您的 ret。 Official docs.

【讨论】:

  • 该链接描述了 comp.属性和方法。使用方法可以解决这个问题,但它不是理想的解决方案,因为方法不使用缓存。
  • 谢谢!我也用过this.$forceUpdate()
  • @WilsonFreitas 我想这是问题中提到的首先导致此问题的缓存。
  • @AbrahamBrookes 我认为 OP 的问题与他更新组件状态的方式有关。 Vue 无法检测到他的更改,因此组件不会对它们做出反应。如果根据 Vue 反应性指南更新状态,缓存应该不是问题。
  • @WilsonFreitas 在处理了类似问题之后,我认为您是正确的。当 OP 使用 Object.assign() 时,他们正在向对象添加未跟踪的属性。我想我可以回答这个问题。
【解决方案3】:

当值为undefined时我遇到了这个问题,然后计算无法检测到它的变化。我通过给它一个空的初始值来修复它。

根据 Vue 文档

【讨论】:

  • 这是对无法响应的数据变量的正确解释,但不适用于计算属性,这就是这个问题的意义所在。
【解决方案4】:

您需要为 v-for 中的列表项分配一个唯一的键值。就这样..

<ClassView :klass-raw="klass" :key="klass.id"/>

否则,Vue 不知道要更新哪些项目。这里解释https://vuejs.org/v2/guide/list.html#key

【讨论】:

  • 即使组件没有在 v-for 中使用,当属性更新时,我已经为我解决了这个问题。
【解决方案5】:

如果你在返回前添加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 属性”。

【讨论】:

    【解决方案6】:

    对于这种情况,我有一个解决方法,我不知道你是否喜欢。我在data() 下放置了一个整数属性(我们称之为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;
      },
    },
    };
    

    这里是link

    【讨论】:

    • 如果从组件外部更改对象怎么办?
    • 您的意思是通过在组件上使用ref 来更改对象吗?然后,您可以像更改对象一样更改trigger 的值。
    【解决方案7】:

    如果您要向返回的对象添加属性 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://vuejs.org/v2/guide/reactivity.html

    【讨论】:

      【解决方案8】:

      对于其他在 Vue3 上遇到此问题的人,我刚刚解决了它,并且能够通过包装我从 setup() 函数返回的值来摆脱我之前需要的所有 this.$forceUpdate()-s [和需要反应] 在引用中使用提供的 ref() 函数,如下所示:

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

      执行此操作后,对包装值的每次更改都会立即反映出来!

      【讨论】:

        【解决方案9】:

        我有同样的问题,因为对象不是反应性的,因为我通过这种方式更改数组:arrayA[0] = valuearrayA 已更改,但从 arrayA 计算的计算值未触发。例如,您需要使用$set,而不是为arrayA[0] 赋值。 您可以通过阅读下面的链接深入了解 https://vuejs.org/v2/guide/reactivity.html

        我还使用了一些技巧,例如在计算中添加缓存 = false

        compouted: {
           data1: {
              get: () => {
                 return data.arrayA[0]
              },
              cache: false
           }
        }
        

        【讨论】:

          猜你喜欢
          • 2019-06-18
          • 1970-01-01
          • 2016-04-04
          • 1970-01-01
          • 2019-04-18
          • 2018-01-25
          • 1970-01-01
          • 2020-08-11
          相关资源
          最近更新 更多