Vue.js computed property not updating

2019-04-06 02:31发布

I'm using a Vue.js computed property but am running into an issue: The computed method IS being called at the correct times, but the value returned by the computed method is being ignored!

My method

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

The values printed out by the console.log statement are correct, but when I use filteredClasses in template, it just uses the first cached value and never updates the template. This is confirmed by Vue chrome devtools (filteredClasses never changes after the initial caching).

Could anyone give me some info as to why this is happening?

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>

3条回答
Lonely孤独者°
2楼-- · 2019-04-06 02:37

If your intention is for the computed property to update when project.classes.someSubProperty changes, that sub-property has to exist when the computed property is defined. Vue cannot detect property addition or deletion, only changes to existing properties.

This has bitten me when using a Vuex store with en empty state object. My subsequent changes to the state would not result in computed properties that depend on it being re-evaluated. Adding explicit keys with null values to the Veux state solved that problem.

I'm not sure whether explicit keys are feasible in your case but it might help explain why the computed property goes stale.

Vue reactiviy docs, for more info: https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats

查看更多
走好不送
3楼-- · 2019-04-06 02:42

You need to assign a unique key value to the list items in the v-for. Like so..

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

Otherwise, Vue doesn't know which items to udpate. Explanation here https://vuejs.org/v2/guide/list.html#key

查看更多
女痞
4楼-- · 2019-04-06 03:00

I've ran into similar issue before and solved it by using a regular method instead of computed property. Just move everything into a method and return your ret. Official docs.

查看更多
登录 后发表回答