vue数据响应式原理(一)

269 阅读3分钟

1.通过options获取vue实例的参数

new Vue({...})
function Vue(options) {
  this._init(options); // 入口方法,初始化操作
}

2.初始化方法initMixin

  • Vue原型上添加_init方法
  • Vue中的状态进行初始化initState(双向绑定)
function initMixin(Vue) {
  Vue.prototype._init = function(options) {
    const vm = this;
    vm.$options = options;
    // 初始化状态(将数据做一个初始化劫持,当我改变数据时应该更新视图)
    // vue组件中又很多的状态 data props watch computed
    initState(vm)
}

3.初始化状态

initProps/initMethods/initData/initComputed/initWatch

4.initData数据劫持方案

function initData(vm) {
  let data = vm.$options.data;
 // 执行data函数  返回对象
  vm._data = data = typeof data == 'function' ? data.call(vm) : data;
  // 数据的劫持方案
  observe(data)

  // 属性代理  当我去vm上取值时,帮我将属性的取值代理到vm._data上
  for(let key in data) {
    proxy(vm,'_data',key)
  }

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

5.observe函数

function observe(data) {
  //  判断data是不是object 如果不是就不需要做数据劫持了
  if (typeof data !== 'object' || data == null) {
    return;
  }
 //  __ob__对象观测标志
  if(data.__ob__return data;
  return new Observer(data)
}

6.Observer数据观测核心类

class Observer {
  constructor(value) {
    // 判断一个对象是不是被观测过,看他是否有__ob__这个属性
    Object.defineProperty(value,'__ob__',{
        enumerable:false// Object.keys获取不到这个key值 就不会导致死循环
        configurable:false,
        value: this
      })
    // 数组和对象采用不同的观测方式
    if(Array.isArray(value)) {
      // 开发功能时很少对数组索引进行操作,为了性能考虑不对数组进行拦截
      // 拦截可以改变数组的方法进行操作(push shift unshift splice sort reverse pop)
      // 函数劫持/切片编程
      value.__proto__ = arrayMethods // 重写了数组方法
      // 观测数组中的对象
      this.observeArray(value) // 数组中如果存在对象 也需要被观测
    }else {
      this.walk(value)  // 对象观测
    }
  }
  observeArray(value) {
    value.forEach(item => {
      observe(item) // 观测数组中的对象类型
    })
  }
  walk(data) {
    let keys = Object.keys(data) // 获取对象的key
    keys.forEach(key=>{
      defineReactive(data,key,data[key])  // Vue.util.defineReactive  对象数据劫持方法
    })
  }
}

function defineReactive(data,key,value) {
  observe(value)  // 如果值是对象再进行观测(递归)
  Object.defineProperty(data,key,{
    get() {
      console.log('取值时数据响应');
      return value
    },
    set(newValue) {
      console.log('设置值时数据响应');
      if(newValue == value) return;
      observe(newValue) // 如果用户将值设置为对象仍然需要进行观测
      value = newValue;
    }
  })
}

7.arrayMethods数组方法重写

// 获取数组原型上的方法
let oldArrayProtoMethods = Array.prototype;
// 继承数组原型方法 arrayMethods.__proto__ = oldArrayProtoMethods
export let arrayMethods = Object.create(oldArrayProtoMethods);
let methods = [
  'push',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse', 
  'pop'
] // 这其中方法会改变原数组的值

// 函数劫持/切片编程
methods.forEach(method => {
  arrayMethods[method] = function (...args) {
    console.log('数组方法被调用了');
    const result = oldArrayProtoMethods[method].apply(this,args); // this 就是observer里的value
    let inserted;
    let ob = this.__ob__;
    switch(method) {
      case 'push':  // arr.push({a:1},{b:2})
      case 'unshift'// 追加数据  可能是对象 应该再次被劫持
        inserted = args;
        break;
      case 'splice'// vue.$set原理
        inserted = args.slice(2);
      default:
        break;
    }
    if(inserted) ob.observeArray(inserted) // 新增对象也被观测了
    return result;
  }
})

总结

几个重点:

  1. 对象和数组的数据劫持分开处理

因为数组很少通过索引去修改值,更常见的是使用方法(push等),因此需要重写数组的原型方法

  1. 用户添加的数据可能是对象或者数组,需要额外进行观测

在对象进行赋值时,在set里对新的值继续进行观测;数组添加值时,进行数组的观测

  1. Object.defineProperty在进行对象数据观测时,深层级的对象无法实现观测

因此在defineReactive函数内部继续观测value(observe(value)),通过递归的方式实现数据观测---深度遍历

  1. 数组中可能存在对象,仍然需要进行数据观测
observeArray(value) {
    value.forEach(item => {
      observe(item) // 观测数组中的对象类型
    })
  }
  1. 被观测数据上的__ob__属性有着重要的作用
  • 1)防止数据进行重复的观测
  • 2)__ob__属性指向Observer实例本身,因此在数组劫持的模块里就可以通过被劫持对象的__ob__属性获取Observer实例,从而调取Observer实例上的observeArray方法