浅入浅出的Vue (1) 数据劫持

137 阅读3分钟

Vue使用的是 MVVM 模式,当Model层改变,View 层也会改变,Vue使用数据劫持来实现对数据的检测,本篇文章将实现一个简易的方法来模仿Vue中的数据劫持。

数据劫持主要分为两类,一类是对象,另一类是数组,首先介绍对象的数据劫持

1 对象的数据劫持

1.1 模拟使用Vue时的数据

劫持 data 对象

const vm = new Vue({
  el: '#app',
  data: {
    name: 'Matt',
    jobInfo: {
      address: 'nanjing',
      title: 'worker'
    }
  }
})

1.2 定义observe方法

只有对象类型才进行劫持,而基本类型不进行劫持,直接略过

function observe(data) {
  // 判断data的类型,只有对象类型才进行劫持
  let type = typeof data
  if (type != 'object' && data == null) {
    return false
   } 
  return new Observe(data)
}

1.2 定义 Observe 类

class Observe {
  constructor(data) {
    this.walk(data)
  }
  // 遍历 data
  walk(data) {
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })

  }
  // 对数据进行劫持
  defineReactive(obj, key, value) {
    Object.defineProperty(obj, key, {
      get() {
        return value
      },
      set(newValue) {
        value = newValue
      }
    })
  }
}

1.3 引发的两个问题

1.3.1 data中如果有值为对象不会被劫持

运行以后,发现只有data中的属性进行了劫持,而 jobInfo 中的属性没有被劫持

observe01.png

解决办法:

defineReactive(obj, key, value) {
    // 在劫持之前,对属性值进行递归 observe
    observe(value)
    Object.defineProperty(obj, key, {
      get() {
        return value
      },
      set(newValue) {
        value = newValue
      }
    })
  }
1.3.2 直接更改属性为对象,也无法被劫持
// 修改 data 中的属性
data.name = {
  fulltName:'Matt',
}

observe02.png

解决办法:

defineReactive(obj, key, value) {
    // 在劫持之前,对属性值进行递归 observe
    observe(value)
    Object.defineProperty(obj, key, {
      get() {
        return value
      },
      set(newValue) {
        // 更改数据时,对属性值进行递归 observe
        observe(newValue)
        value = newValue
      }
    })
  }

2 数组的数据劫持

2.1 劫持数组的问题

当对数组进行劫持时,会对每一项进行劫持,性能会有影响

data.name = [1,2,3,4,5]

observe03.png

2.2 劫持数组每一项

因为对数组每一项进行劫持会有性能影响,所以,vue只对数组中对象进行劫持

class Observe {
  constructor(data) {
    // 判断数组
    if(Array.isArray(data)) {
      this.walkArray(data)
    }else {
      this.walk(data)
    }
  }

  walk(data) {
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }
  // 新增的数组遍历
  walkArray(data) {
    for(let key in data) {
      observe(data[key])
    }
  }

  defineReactive(obj, key, value) {
    observe(value)
    Object.defineProperty(obj, key, {
      get() {
        return value
      },
      set(newValue) {
        // 如果新值也为对象,也需要进行数据劫持
        observe(newValue)
        value = newValue
      }
    })
  }
}

2.3 无法劫持新增项

此时,array.push()等新增的项,无法进行劫持

let data = {
  name: 'Matt',
  jobInfo: {
    address: 'nanjing',
    title: 'worker'
  },
  arr:[{age:
18}]
}
data.arr.push({sex:'boy'})

observe04.png

2.4 更改数组原有方法

export let myArray = Object.create(Array.prototype)

// 这几个方法可以新增项,因此需要进行重写
let methods = ['push','unshift','pop','shift','reverse','sort','splice']

methods.forEach(method => {
  myArray[method] = function (...args) {
    // arr.push(1,2) 此时this==arr
    Array.prototype[method].call(this,...args)
    let insert
    // 新增的一样要有检测
    switch(method) {
      case 'push':
      case 'unshift':
        insert = args
        break;
      case 'splice':
        insert = args.slice(2)
        break;
      default:
        break
    }
    if(insert) {
      // 新增的每一项都需要再次进行 observe __ob__是在Observe中得到
      this.__ob__.arrayObserve(insert)
    }
  }
})
  constructor(data) {
    // 对对象中所有属性进行劫持
    Object.defineProperty(data ,'__ob__',{
      value:this,
      enumerable:false  // 防止爆栈
    })
    if(Array.isArray(data)) {
      data.__proto__ = myArray
      this.arrayObserve(data)
    }else {
      this.walk(data)
    }
  }

3 vue中数据劫持存在的问题

3.1 无法检测对象变动

①直接在对象上添加属性

②删除对象上的属性

data.time = 'sunday'

observe05.png

3.2 无法检测数组变动

①用索引改变值 vm.items[indexOfItem] = newValue

②修改数组长度 vm.items.length = newLength

observe06.png