vue的源码学习——数据的响应式原理

127 阅读1分钟

vue的特点是数据驱动视图,当修改数据时视图随之更新,所以需要对数据进行观测,取值和赋值都要进行观测。

1.首先是new一个Vue的实例

let vue = new Vue({
  data() {
    return {
      a: 1,
      b: [1],
      c: { d: 1 }
    }
  }
})

2.对实例进行初始化

// index.js
function Vue(options) { // options 就是传入的对象
   this._init(options)
}

// init.js
function initMixin(Vue) { // 给vue增加init方法,将_init挂到vue的原型上
  Vue.prototype._init = function (options) {
    const vm = this
    vm.$options = options // 将options放在实例上

    // 初始化状态
    initState(vm)
  }
}

3.初始化状态

// state.js

function initState(vm) { // 初始化的总函数,初始化props,data,watch,methods,computed等等
  const opts = vm.$options // 获取到传入的对象
  
  if (opts.data) {
     initData(vm)
  }
  // ....还有许多初始化的方法
}

function initData(vm){ // 初始化用户传入的data
  let data = vm.$options.data
  typeof data === 'function' ? data.call(vm) : data

  vm._data = data
  // 对数据进行劫持 object.defineProperty
  observe(data)

  // 将vm._data用vm来代理,可以直接通过vm.xxx取值
  for(let key in data) {
    proxy(vm, '_data', key)
  }
}

function proxy(vm, target, key) {
  Object.defineProperty(vm, key, {
    get() {
      return vm[target][key]
    },
    set(newValue) {
      vm[target][key] = newValue
    }
  })
}

4.对数据进行观测

// observe文件 index.js

// 定义观测类 对对象和数组进行观测
class Observe {
  constructor(data) {
    // Object.defineProperty 只能劫持已经定义过的属性

    // 为了能在重写数组方法的函数中获取到Observe的实例, 而且给数据加了标识,证明已经被观测过
    Object.defineproperty(data, '__ob__', {
      value: this,
      enumerable: false, // 设置为不可枚举,避免进入死循环, data.__ob__ = this会导致死循环
    })

    if (Array.isArray(data)) {
      // 如果是数组,重写数组方法(对数组中每项进行劫持,浪费性能)
      // 一般操作数组是比较少使用索引,都是使用数组方法
      // 例如:push, pop, unshift, shift, reverse, sort, splice
      // 这些数组都会改变数组本身
      observeArray(data)
    } else {
      this.walk(data)
    }
  }

  walk(data) { // 观测对象 循环对象 对属性依次劫持
    // 重新定义属性
    Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
  }

  observeArray(data) { // 观测数组
    data.forEach(item => observe(item))
  }
}


function observe(data) { // 观测的入口
  // 对对象进行劫持
  if (typeof data !== 'object' || data === null) {
    return // 只对对象进行劫持
  }

  if (data.__ob__ instanceof Observer) { // 说明这个对象被代理过
    return data.__ob__
  }

  // 如果一个对象被劫持过来就不需要再被劫持了
  return new Observe(data)

}

function defineReactive(target, key, value) { // 对象上所有key的get和set方法处理
  observe(value) // 对所有对象都进行属性劫持
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(newValue) {
      if (newValue === value) return
      observe(newValue) // 修改时新值可能为对象
      value = newValue
    }
  })
}

5.对数组变异方法的重写

所谓的变异方法就是可以改变数组本身的方法:push, pop, unshift, shift, reverse, sort, splice

// observe文件 array.js(重写数组的部分方法)

let oldArrayProto = Array.prototype // 将原先数组的原型保存下来

let newArrayProto = Object.create(oldArrayProto)

let methods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'reverse',
  'sort',
  'splice'
]

methods.forEach(method => {
  newArrayProto[method] = function (...args) { // 外部重写数组的方法
    const result = oldArrayProto[method].call(this, ...args) // 内部调用数组的原生方法
    
    let inserted // 获取插入的数据
    let ob = this.__ob__ // 这里的this就是劫持的对象data

    switch(method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
      default:
        break
    }

    if (inserted) { // 对新增的内容进行观测,新增的内容还是数组
      ob.observeArray(inserted)
    }
    return result
  }
})

6.问题

1.对象的数据劫持只能劫持对象的原有属性,对新增的属性无法劫持,vue使用$set解决这个问题 2.数组修改只能通过改写的方法,无法直接arr[index] = xxx 进行修改,也无法通过length属性进行修改