vue2之监测数据

124 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第11天,点击查看活动详情

前面我们有篇文章主要介绍了vue2数据响应式原理,如果说那篇文章主要是理论篇,那我们这篇文章是实践版,主要是围绕对象和数组的监测,如果机制不能自动及时更新视图,我们该如何手动更新视图。

监测对象

我们在前面已经了解到,vue监测对象是使用Object.defineProperty()来进行数据劫持的,这样就给数据添加了一个代理,每次对象的属性被修改时就会调用setter, 对象的属性被获取的时候就会调用getter。

下面我们简单地实现一下vue的响应式

let obj = {
  name: '广东刘亦菲',
  age: 18,
  job: {
    name: 'coder',
    weekdays: 5
  }
}
// 创建一个监视的实例对象,用于监视data属性的变化
const reactiveObj = new Observer(obj)

let vm = {}
vm._data = obj = reactiveObj

function Observer (obj) {
  // 这个类里面就需要给每个属性都添加响应式
  const keys = Object.keys(obj)
  keys.forEach(key => {
    Object.defineProperty(this, key, {
      get () {
        console.log(`${this}上的${key}正在被读取`)
        return obj[key]
      },
      set (value) {
        console.log(`${this}上的${key}正在被修改`)
        obj[key] = value
      }
    })
  })
}

特别说明一下:

为什么Object.defineProperty()第一个参数是this,而不是obj。是因为这个this拿到的是实例化对象,也就是上述代码中的reactiveObj(代理之后的对象)。如果写成obj的话,那么get里面返回值是obj[key],也就是说再次读取obj的属性值,这样的话就会造成一个死循环,最终导致内存溢出。

监测数组

vue里面监听数组的方式其实是对Array.property上的数组的方法进行了封装。换句话说,在vue里面访问数组的这些方法已经不再是访问数组原型上的那些方法了。具体有以下这些方法

  • push()
  • pop()
  • shift()
  • unshift()
  • sort()
  • splice()
  • reverse()
<template>
  <div class="hello">
    <div>姓名:{{obj.name}}</div>
    <div>年龄:{{obj.age}}</div>
    <ul>
      <li v-for="(item, index) in obj.leaders" :key="index">
        {{item.name}} - {{item.age}}
      </li>
    </ul>
    <button @click="addLeader">添加领导信息</button>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      obj: {
        name: '广东刘亦菲',
        age: 18,
        leaders: [
          {
            name: '一个专政的独裁者',
            age: 40
          },
          {
            name: '一个温柔的管理者',
            age: 35
          }
        ]
      }
    }
  },
  methods: {
    addLeader () {
      let newLeader = {
        name: '仙女领导',
        age: 30
      }
      this.obj.leaders.push(newLeader)
      this.obj.leaders.pop()
      this.obj.leaders.shif()
      this.obj.leaders.unshift(newLeader)
      this.obj.leaders.splice(0, 1)
      this.obj.leaders.sort((a, b) => a - b)
      this.obj.leaders.reverse()
      this.obj.leaders = [{
        name: '仙女领导',
        age: 30
      }]
      this.obj.leaders[0].name = '仙女领导'
    }
  }
}
</script>

上面除了重写的修改数组的七个方法外,重新给数组赋值,或者给数组的某些项的属性重新设置值都是会触发响应式的。

下面我们来看一下具体原理

主要就是继承Array,然后重写数组上的方法

// 继承Array原型上的所有属性
const extendArr = Object.create(Array.prototype)
const arrMethods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'sort',
  'splice',
  'reverse'
]
// 重新包装上述数组的方法
arrMethods.forEach(item => {
  const oldItem = Array.prototype[item]
  const newItem = function (...args) {
    oldItem.apply(this, args)
  }
  extendArr[item] = newItem
})

只有上述这几种方法才具有响应式

手动更新视图

对于对象类的数据,如果初始化data的时候没有写某个属性,而是通过methods里的方法给他添加属性,不是响应的,视图不会及时更新。 对于数组类的数据,如果使用了上面的那七种方法之外的方法,那么视图也是不会刷新的。比如使用了concat, filter等。 对于这种问题,vue给我们提供了一种方法this.$set()。

1. vue.set()

vue.set()是向响应式对象添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为vue无法探测普通的新增属性。

<div>姓名:{{obj.name}}</div>
<div>年龄:{{obj.age}}</div>
<div>工作:{{obj.job}}</div>
<button @click="addJob">添加职位信息</button>

data () {
  return {
    obj: {
      name: '广东刘亦菲',
      age: 18
    }
  }
},
methods: {
  addJob () {
    this.obj.job = 'coder'
    console.log('职位', this.obj.job)
  }
}

效果如下:

image.png

数据改变了,页面视图没有更新。

1.1 使用全局set

语法:

Vue.set( target, propertyName/index, value )
<script>
import Vue from 'vue'
export default {
  name: 'HelloWorld',
  data () {
    return {
      obj: {
        name: '广东刘亦菲',
        age: 18
      }
    }
  },
  methods: {
    addJob () {
      Vue.set(this.obj, 'job', 'coder')
      console.log('职位', this.obj.job)
    }
  }
}
</script>

效果如下:

image.png

1.2 使用组件set

语法格式

vm.$set( target, propertyName/index, value )
<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      obj: {
        name: '广东刘亦菲',
        age: 18
      }
    }
  },
  methods: {
    addJob () {
      this.$set(this.obj, 'job', 'coder')
      console.log('职位', this.obj.job)
    }
  }
}
</script>

效果如上图

我们在实际开发项目中,主要还是组件化开发,所以使用this.$set()的情况居多。

需要注意的是对象不能是vue实例,或者vue实例的根数据对象。 上述代码示例都是以对象为例的,数组的使用方式也是一样的。

小结

vue监视数据的原理:

  1. vue会监视data所有对象的所有层级
  2. 监测对象中的数据: 通过setter实现监视,且要在new Vue时就传入要监测的数据
  • 对象中后追加的属性,vue默认不做响应式处理
  • 如需给后添加的属性做响应式,请使用:Vue.set( target, propertyName/index, value )或者vm.$set( target, propertyName/index, value )
  1. 监测数组中的数据: 通过包裹数组更新元素的方法实现,本质上就做了两件事:
  • 调用原生对应的方法对数组进行更新
  • 重新解析模板,进而更新页面
  1. 在vue中修改数组中某一项一定要用下面的方法:
    • 使用push(), pop(), shift(), unshift(), sort(), splice(), reverse()
    • 使用Vue.set()或者vm.set()需要注意的Vue.set()或者vm.set() 需要注意的Vue.set()或者vm.set()不能是vue实例,或者vue实例的根数据对象。