$set后数据改变但是动态绑定的类样式并没有发生改变(vue的响应式问题)

lang1427
  • 23
<template>
  <div class="comment-main" v-if="commentData.length!==0">
    <div class="scroll-content">
      <div class="list-items" v-for="(comment,cIndex) of commentData" :key="comment.commentId">
        <div class="user-baseinfo">
          <div class="liked">
            <span
              @click="setLikeComment(cIndex)"
              :class="comment.commentLiked ? 'fa-thumbs-o-up liked-active' : 'fa-thumbs-o-up' "
            >{{ comment.commentLikedCount !== 0 ? comment.commentLikedCount : '' }} &nbsp;&nbsp;</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

 <script>
export default {
  data() {
    return {
      comment: [
        {
          userId: 1599056,
          userName: "请留步施主",
          userAvatar:
            "https://p4.music.126.net/Nw7PvBrD6dAef-pCS8vrDw==/109951164690544939.jpg",
          commentId: 3325604169,
          commentContent: "哈哈",
          commentTime: 1590647018680,
          commentLiked: false,
          commentLikedCount: 0,
          parentCommentId: 0,
        },
      ...假设这里有20+数据 
        {
          userId: 352679291,
          userName: "预渡風华",
          userAvatar:
            "https://p4.music.126.net/dUskbMgLMSSJ2GYvboZLCA==/109951165021317989.jpg",
          commentId: 3325315601,
          commentContent:
            "一个本一个听着国内流行小曲的小伙子 因为网易云音乐渐渐的走向了歧途(正道) (手动滑稽)",
          commentTime: 1590627790051,
          commentLiked: true,
          commentLikedCount: 3,
          parentCommentId: 0,
        }
      ]
    };
  },
  computed: {
    commentData() {
      return this.comment.filter(item => {
        return item.parentCommentId === 0;
      });
    }
  },
  methods: {
     setLikeComment(index) {
      // this.commentData[index].commentLiked = true
      this.$set(this.commentData, index, {
        ...this.commentData[index],
        commentLiked: true
      });
      // this.$forceUpdate();
      
       console.log(this.commentData[index])
    }
  }
};
</script>
<style>
.liked-active {
  color: red;
}
</style>

很疑惑

this.commentData[index].commentLiked = true

这个居然直接完成了响应式 (数据和视图同时发生了改变)

而 $set 只改变了数据并没有改变视图
当添加

this.$forceUpdate()

;之后视图才发生改变

请问:

  1. Vue的响应式原理不是不能通过index进行改变值而达到响应的呢?
  2. $set的响应式:为什么在这里会无效?
  3. 可以使用 this.$forceUpdate()进行解决这个问题,它的作用以及是否会产生副作用,有更好的解决方式吗?

当然我Vue的版本是2.6.10

回复
阅读 2.9k
3 个回答
✓ 已被采纳

关于错误的点,上面已经说了。
在这里补充一点,代码未触发报错的原因在于,并未直接操作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内部的报错。

问题解析

  1. Vue的响应式原理不是不能通过index进行改变值而达到响应的呢?
  2. $set的响应式:为什么在这里会无效?
  3. 可以使用 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>

image.png

那所谓的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。

以上纯属个人理解,如有错误,欢迎指出,谢谢!

说实在的,我还没见过直接改变计算属性的值的。计算属性的值是依赖其中使用的变量的,如果想要改变计算属性,应该改变其使用的变量

直接更改computed会报错的, 因为默认computed只有get方法, 你这个没报错估计是用的生产环境的vue而不是开发环境的vue。试着改动comment,而非`
commentData

宣传栏