我正在使用 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
时,它只使用first 缓存值 并且从不更新模板。 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.someSubProperty
更改时更新,则在定义计算属性时该子属性必须存在。 Vue 无法检测属性的添加或删除,只能检测现有属性的更改。
当使用带有空
state
对象的 Vuex 存储时,这让我很头疼。我对状态的后续更改不会导致依赖于它被重新评估的计算属性。向 Veux 状态添加具有空值的显式键解决了这个问题。
我不确定显式键在您的情况下是否可行,但这可能有助于解释为什么计算属性会过时。
Vue reactiviy 文档,了解更多信息: https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
我以前遇到过类似的问题,并通过使用常规方法而不是计算属性来解决它。只需将所有内容移动到一个方法中并返回您的 ret。 官方文档
注意:请参阅下面的更新,而不是此解决方法。
对于这种情况,我有一个解决方法,不知道你是否喜欢。我在
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;
},
},
};
更新:在官方文档中找到了更好的方法,因此您不需要像
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);
},
},
};
这里有一个链接
如果你在返回前添加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 提供一个键属性”。
您需要为 v-for 中的列表项分配一个唯一的键值。像这样..
<ClassView :klass-raw="klass" :key="klass.id"/>
否则,Vue 不知道要更新哪些项目。这里解释https://v2.vuejs.org/v2/guide/list.html#key
我有同样的问题,因为对象不是反应性的,因为我通过这种方式更改数组:
arrayA[0] = value
。 arrayA
已更改,但从 arrayA
计算的计算值不会触发。例如,您需要使用 arrayA[0]
,而不是为 $set
赋值。
您可以通过阅读下面的链接进行更深入的研究
https://v2.vuejs.org/v2/guide/reactivity.html
我还使用了一些技巧,比如在 computed 中添加一个 cache = false
compouted: {
data1: {
get: () => {
return data.arrayA[0]
},
cache: false
}
}
对于其他在 Vue3 上遇到这个问题的人,我刚刚解决了它,并且能够通过包装我从
this.$forceUpdate()
函数返回的值来摆脱我之前需要的所有 setup()
-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
}
},
...
}
完成此操作后,对包装值的每个更改都会立即反映出来!
如果您要向返回的对象添加属性 在 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
添加到 vues 反应系统中。
要回答您的问题,您需要在before将它们传递给vue之前构建具有所有反应属性的对象——即使它们只是
null
。任何在 vues 反应系统上苦苦挣扎的人都应该阅读这个链接:https://v2.vuejs.org/v2/guide/reactivity.html