关于错误的点,上面已经说了。
在这里补充一点,代码未触发报错的原因在于,并未直接操作computed数据。
先展示下报错是怎样的:
vue.runtime.esm.js?2b0e:619 [Vue warn]: Computed property "commentData" was assigned to but it has no setter
computed中的数据在Object.defineProperty中创建了get, set设为noop, 即(function noop(a,b,c){}
)
代码中写的是 this.commentData[index].commentLiked = true
, 这里可以看做3部分
- this.commentData[index] 这里只是调用了get 读取commentData中index的值
-
this.commentData[index]
.commentLiked 调用了this.commentData[index]
的get
-
this.commentData[index]
.commentLiked = true调用了this.commentData[index]
的set
因此,并未直接调用this.commentData的set,所以未触发报错。
如果要触发这个报错,怎么去做呢?
this.commentData = [{commentLiked: true}]
如此,直接调用commentData的set方法,便会触发vue内部的报错。
问题解析
- Vue的响应式原理不是不能通过index进行改变值而达到响应的呢?
- $set的响应式:为什么在这里会无效?
- 可以使用 this.$forceUpdate()进行解决这个问题,它的作用以及是否会产生副作用,有更好的解决方式吗?
下面例子将采取简写的方式,不再采用你提供的demo
问题1
这句话说的不够准确。
如果说要通过修改index, 那么vue 可以通过修改index 实现数据改变。
<template>
<div>
<ul>
<li v-for="(item, key) in arr" :key="key" @click="handleClick(key)">
<p>
姓名:
<b>{{ item.name }}</b>
</p>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "Home",
data() {
return {
arr: [
{
name: "Tom"
},
{
name: "Pony"
}
]
};
},
methods: {
handleClick(key) {
this.arr[key].name = "Allas";
console.log(this.arr);
}
}
};
</script>
<style lang="scss" scoped>
li {
padding: 12px 20px;
font-size: 16px;
cursor: pointer;
border: 1px solid #ccc;
}
</style>
那所谓的vue数组问题,应该是什么呢?
handleClick(key) {
this.arr[key]= {
name: "Allas"
}
}
直接修改调用arr.set方法,而Object.defineProperty又无法监听数组长度的变化,故这里数据会发生改变,但无法触发页面渲染。
使用computed后(问题1生效,问题2不生效)
<template>
<div>
<ul>
<li v-for="(item, key) in list" :key="key" @click="handleClick(key)">
<p>
姓名:
<b>{{ item.name }}</b>
</p>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "Home",
data() {
return {
arr: [
{
name: "Tom"
},
{
name: "Pony"
}
]
};
},
computed: {
list() {
return this.arr.filter(item => item.name !== "");
}
},
methods: {
handleClick(key) {
// 方案一
this.list[key].name = 'Allas'
// 方案二
// this.list[key] = {
// name: "Allas"
// };
// 方案三
// this.$set(this.list, key, {
// ...this.list[key],
// name: "Allas"
// });
console.log(this.list);
}
}
};
</script>
<style lang="scss" scoped>
li {
padding: 12px 20px;
font-size: 16px;
cursor: pointer;
border: 1px solid #ccc;
}
</style>
初始化数据时,computed 会遍历内部值,通过Object.defineProperty监听每个值,并会执行new Watcher,简写代码如下:
for(const key in computed) {
const userDef = computed[key]
// key 为list, userDef为() =>this.arr.filter(item => item.name !== "")
const getter = typeof userDef === 'function' ? userDef : userDef.get
// getter 即为() =>this.arr.filter(item => item.name !== "")
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
// 注册watcher
Object.defineProperty(target, key, sharedPropertyDefinition)
// sharedPropertyDefinition上面说过,注册了get方案,未注册set方案。
/* PS: 非production环境下,set为
() => {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
*/
}
故方案一调用this.list[key]
.name, 调用前半部分触发get,返回arr的值,
此时就变成了this.arr[key].name
而arr位于data中,它本身也被绑定成响应式数据,故这种就跟上面的例子的原因是一致的。
方案二调用this.list[key]
,调用前半部分触发get,返回arr的值
此时就变成了this.arr[key]={name: "Alias"}
而这里直接通过数组下标修改数组内的值,故数据改变,页面不渲染
//$set方法
export function set(target, key, val) {
// 省略部分代码
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 省略部分代码
}
这里target 为this.list,即为数组,故这里调用target.splice修改数据,这就解释了数据为什么改变了。
剩下的就是页面未渲染。
虽说vue重写了数组中部分方法,但重写过程仅在Observer
生成响应式数据中可用(即data等),而computed,未调用Observer,其内部直接调用了Object.defineProperty和Watcher方法,故这里的splice为数组的原生方法,因此这里并不会触发页面渲染,也就解释了set无效的原因。
vue 不支持监听数组长度的变化,而其中某些可用,是因为内部实现了一系列方法。
问题3
$forceUpdate
迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
针对于上面未页面未重新渲染的地方,$forceUpdate可用强行触发更新。
但强行触发更新,会触发实例本身所有数据重新更新,也存在性能浪费问题。
一般来说,尽可能从代码角度避免使用该方法,以及针对性进行性能优化。
但是,一旦组件变得复杂起来,该方法有时也会必不可少。对于这种情况,Welcome to Vue3。
以上纯属个人理解,如有错误,欢迎指出,谢谢!