深入vue2响应式原理,在对象或数组新增属性无响应

发布时间 2023-05-30 08:23:43作者: 布衣梦蝶1978

深入vue2响应式原理,在对象或数组新增属性无响应

解决方法

前言

    该问题只存在vue2, 基于Object.defineProperty的特性,vue3中的proxy已经解决了该问题,但也存在兼容性问题,
例如IE系统任意版本都不支持.

image-20230524090139099

vue2是如何追踪数据变化形成响应

口水版:页面一进来会扫描数据,实行类似双向绑定,当初始时没有设定好属性,后面添加新属性,会存在数据中,但页面并不会进行响应同步(MVVM)
专业版 :
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

image-20230524090114595

vue2响应的注意点

由于 JavaScript 的限制(其实是Object.defineProperty),Vue 不能检测数组和对象的变化。
比方日常开发中遇到需要在对象中添加新的属性,属性已经存在数据中,但页面并不会立即更新.
如下,点击按钮时,'山竹'会变成'杀生丸',但age会在数据中,不会立刻出现在页面

 <div id='app'>
    <p>{{ user.name }}</p>
    <p>{{ user.age }}</p>
    <button @click='changeName'>改变data中name属性</button>
    <button @click='addAge'>给data添加age属性</button>
  </div>
  <script>
    let vm = new Vue({
      el: '#app',
      data() {
        return {
          user: { name: '山竹' }
        }
      },
      methods: {
        changeName() {
          //如果是一开始就设定属性,绑定页面后,点击会触发
          this.user.name = '杀生丸'
        },
        addAge() {
          //非响应
          this.user.age = 18
          console.log(this.user);//age: 18,name: "杀生丸"
        }
      }
    })
  </script>

解决方案

其解决方法多种多样,这里举例几种,但根本的中心点就是使对象/数组发生变更,触发watcher重新渲染

对象

方案一:初始时设定

如下,在一开始的时候就预设可能需要添加的属性, age: ''
<div id='app'>
    <p>{{ user.name }}</p>
    <p>{{ user.age }}</p>
    <button @click='changeName'>改变data中name属性</button>
    <button @click='addAge'>给data添加age属性</button>
  </div>
  <script>
    let vm = new Vue({
      el: '#app',
      data() {
        return {
          user: { name: '山竹', age: '' }
        }
      },
      methods: {
        changeName() {
          //如果是一开始就设定属性,绑定页面后,点击会触发
          this.user.name = '杀生丸'
        },
        addAge() {
          this.user.age = 18
          console.log(this.user);//age: 18,name: "杀生丸"
        }
      }
    })
  </script>

方案二:调用Vue.set方法

Vue.set(原对象,需要设置的新属性, 需要设置的新值)
 <div id='app'>
    <p>{{ user.name }}</p>
    <p>{{ user.age }}</p>
    <button @click='changeName'>改变data中name属性</button>
    <button @click='addAge'>给data添加age属性</button>
  </div>
  <script>
    let vm = new Vue({
      el: '#app',
      data() {
        return {
          user: { name: '山竹' }
        }
      },
      methods: {
        changeName() {
          //如果是一开始就设定属性,绑定页面后,点击会触发
          this.user.name = '杀生丸'
        },
        addAge() {
          // Vue.set(原对象/数组,需要设置的新属性, 需要设置的新值)
          Vue.set(this.user, 'age', 18)
          console.log(this.user);//age: 18,name: "杀生丸"
        }
      }
    })
  </script>

方案三:创建一个新的对象,替换原对象

这种方法可以用于需要添加多个新属性,再把原对象与新属性合并到新对象中 Object.assign(目标对象,原对象, 新属性)
<div id='app'>
    <p>{{ user.name }}</p>
    <p>{{ user }}</p>
    <button @click='changeName'>改变data中name属性</button>
    <button @click='add'>给data添加新属性</button>
  </div>
  <script>
    let vm = new Vue({
      el: '#app',
      data() {
        return {
          user: { name: '山竹' }
        }
      },
      methods: {
        changeName() {
          //如果是一开始就设定属性,绑定页面后,点击会触发
          this.user.name = '杀生丸'
        },
        add() {
          // Object.assign方法的第一个参数是目标对象,后面的参数都是源对象
          // Object.assign(目标对象,原对象, 新属性)
          this.user = Object.assign({}, this.user, { skill: '铁碎牙', age: 18 })
          console.log(this.user);//age: 18,name: "山竹",skill: "铁碎牙"
        }
      }
    })
  </script>

数组

方案一:切割替换原数组

//vm.items.splice(下标, 1, 新数组)
vm.items.splice(indexOfItem, 1, newValue)

方案二:Vue.set

//Vue.set(原数组,需要设置的下标, 需要设置的新值)
Vue.set(vm.items, indexOfItem, newValue)