vue响应式原理

123 阅读3分钟

vue源码学习记录

wallhaven-y8wqeg.jpeg

1、初始化数据

function initData(vm) {
  let data = vm.$options.data // 用户传入的数据

  // 如果用户传递的是一个函数 则取函数的返回值作为对象 如果就是对象就直接使用这个对象
  // 只有根实例的data可以是一个对象

  data = vm._data = isFunction(data) ? data() : data;
  
  // 需要将data变成响应式的 Object.defineProperty 重写data中的所有属性
  observe(data) // 观测数据

  // 实例取值代理 即实现vm.message === vm.data.message
  // 由于直接遍历data属性进行重写 造成性能消耗严重,所以利用_data做一个转换 vm.data.message === vm._data.message

  for (let key in data) {
    // 对data上的数据做一次代理
    proxy(vm, key, '_data')
  }
  // console.log(data)
}

2、属性递归劫持

class Observer {
  constructor(value) {
      this.walk(value)
  }
  walk(data) {  
    // 循环对象
    Object.keys(data).forEach(item => {
      // 对每个属性重新定义
      defineReactive(data,item,data[item])
    })
  }
}
function defineReactive(obj,key,value) {
  observe(value) // 处理嵌套的对象属性 递归进行观测数据,不管有多少层,我都进行defineProperty
  Object.defineProperty(obj,key, {
    set(newValue) { // 如果设置的值是一个对象也需要再次劫持
      if (newValue === value) return 
      console.log('修改')
      observe(newValue) // 如果重新赋值的值是一个对象也要监听
      value = newValue
    },
    get () {
      return value
    }
  })
}
export function observe(value) {
  // 如果不是对象 不做处理
  if (!isObject(value)) {
    return
  } 
  // 判断数据是否被观测过 避免重复观测
  return new Observer(value)
}

3、数组方法的劫持

import { arrayMethods } from "./array";
class Observer {
  constructor(value) {
    // 添加自定义属性 为了让改写数组原型方法的时候能使用Observer类上的方法
    Object.defineProperty(value,'__ob__',{
      value: this,
      enumerable: false // 标识这个属性不能被列举出来 不能被循环到
    })
    // 对value进行判断 判断是否是数组
    if(isArray(value)) { // 如果是数组就改写原型链
      // 更改数组原型方法
      value.__proto__ =  arrayMethods // 重写数组的方法
      // 如果是嵌套数组 需要递归处理 [[]] [{}]
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  observeArray(data) { // 嵌套数组里面的项需要再次监听
    data.forEach(item => observe(item))
  }
  walk(data) {  
    // 循环对象
    Object.keys(data).forEach(item => {
      // 对每个属性重新定义
      defineReactive(data,item,data[item])
    })
  }
}

重写数据原型方法

let oldArrayPrototype = Array.prototype
export let arrayMethods = Object.create(oldArrayPrototype) // 让arrayMethods 通过__prtoto__能获取到数组的方法

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

methods.forEach(method => {
  arrayMethods[method] = function (...args) {
    // 数组新增的属性要看一下是不是对象 如果是对象 也要劫持

    // 需要调用数组的原生逻辑
    const result = oldArrayPrototype[method].call(this,...args)
    // 可以添加自己逻辑 函数劫持 切片
    let inserted = null
    let ob = this.__ob__;
    switch (method) {
      case 'splice':
        inserted = args.slice(2)
        break;
      case 'push':
      case 'unshift': 
       inserted = args
    }
    if (inserted) ob.observeArray(inserted)
    return result
  }
})

避免重复监听数据

export function observe(value) {
  // 如果不是对象 不做处理
  if (!isObject(value)) {
    return
  } 
  if (value.__ob__) { // 监听过了不再重复监听
    return
  }
  // 判断数据是否被观测过 避免重复观测
  return new Observer(value)
}

综上:vue响应式数据是在拿到用户传入的数据后,将数据分为数组和对象,对象直接通过defineProperty对里面的每一个属性进行观测,嵌套数据就递归观测,如果是数组,要对数组的变异方法进行劫持重写,嵌套数据同样进行递归

对于实例数据访问vm.message能直接通过vm实例访问,是因为源码中做了代理操作,将观测后的data对象通过自定义_data转化后进行代理,将data中的数据“拷贝”到了vm对象上