2. 初始化vue(数据劫持)

140 阅读2分钟

2. 初始化vue数据劫持

vue2中对象的数据劫持:使用到Object.defineProperty方法,使用observe来监听属性的变化

缺点:

  1. 只能对对象中的一个属性进行劫持
  2. 需要遍历
  3. 需要递归

vue2中对数组的数据劫持:使用到函数劫持(重写数组的方法)

所新增代码以及新建目录:

- src
 - index.js
 - init.js
 - initState.js
 - observe // 监听器
  - index.js
  - arr.js

src/index.js:

import { initMixin } from "./init"
/**
 * @author xwya
 * @since 2023-12-11
 * @description  Vue 构造函数
 * @param {Object} options - Vue 的初始化选项。
 * @returns {void} - 没有返回值。
 */
function Vue(options) {
  // 初始化
  this._init(options);
}
initMixin(Vue)
​
export default Vue;

image.png src/init.js:

import { initState } from "./initState";
/**
 * @description 初始化vue
 * @param {Object} Vue
 * @returns {void}
 */
export function initMixin(Vue) { 
  Vue.prototype._init = function (options) { 
    let vm = this
    vm.$options = options
    // 初始化状态
    initState(vm)
  }
}
​

src/initState.js:

import { observe } from "./observe/index";
/**
 * 初始化所有状态
 * @param {Object} vm
 * @returns {void}
 */
export function initState(vm) { 
  let ops = vm.$options
  console.log(ops);
  // 判断
  if (ops.props) { 
    initProps(vm)
  }
  if (ops.data) { 
    initData(vm)
  }
  if (ops.watch) { 
    initWatch(vm)
  }
  if (ops.computed) { 
    initComputed(vm)
  }
  if (ops.methods) { 
    initMethods(vm)
  }
}

function initProps(vm) { 

}

function initData(vm) {
  let data = vm.$options.data
  // 判断data是函数还是对象
  // 是函数的话要解决data指向问题 指向vue实例上去 没有call(vm)的话指向的是window
  data = vm._data=typeof data=== "function" ? data.call(vm) : data
  // data数据进行劫持
  // 将data上的所有属性代理到vm 上
  for (let key in data) { 
    proxy(vm, "_data", key)
  }
  observe(data)

  
}

function initWatch(vm) {
}

function initComputed(vm) { 

}

function initMethods(vm) { 

}

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

image.png

src/observe/index.js:

import { arrayProto } from "./arr";
​
/**
 * @description 用于监听属性的变化通知订阅者
 * @param {Object} data 
 * @returns {Object}
 */
export function observe(data) { 
​
  // 判断是否是对象
  if (typeof data !== 'object' || data === null) { 
    return data;
  }
  return new Observe(data);
​
} 
​
​
class Observe { 
  constructor(data) { 
    Object.defineProperty(data, '__ob__', {
      enumerable: false, // 不能进行枚举
      value:this
    })
    // 判断对象是否是数组
    if(Array.isArray(data)) {
      data.__proto__ = arrayProto;
      // 如果数组是个对象
      this.observeArray(data);  // 处理数组对象
    } else { 
      // 遍历
      this.walk(data);
    }
   
     
  }
  walk(data) { 
    // 遍历data数据,对每个属性进行监听
    let keys = Object.keys(data);
    for (let i = 0, len = keys.length; i < len; i++) { 
      // 对每个属性进行劫持
      let key = keys[i];
      let val = data[key];
      // 监听
      defineReactive(data, key, val);
    }
  }
   // 对数组对象进行遍历监听
  observeArray(data) { 
    for (let i = 0,len=data.length; i < len; i++) { 
      observe(data[i])
    }
  }
​
}
​
// 对对象中的属性进行劫持
function defineReactive(data, key, value) { 
  observe(value) // 深度监听
  // 代理数据
  Object.defineProperty(data,key,{
    enumerable:true, // 可以枚举
    configurable:true, // 可以删除
    get() {
      return value;
    },
    set(newValue) {
      if (newValue !== value) {
        observe(newValue) // 如果新的值也是对象也需要进行监听
        value = newValue;
​
      }
    }
  })
}

src/observe/arr.js:

/* 
  重写数组函数方法
  (1) 获取原来数组方法
  (2) 继承
  (3) 劫持数组方法
*/// (1) 获取原来数组
let oldArrayprotoMethods = Array.prototype; 
// (2) 继承
export let arrayProto = Object.create(oldArrayprotoMethods);
​
// 需要劫持的数组方法
let arrayProtoMethods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];
// (3) 劫持数组方法
arrayProtoMethods.forEach(item => { 
  arrayProto[item] = function (...args) { 
​
    let arr = oldArrayprotoMethods[item].apply(this, args);
    let inserted =null
    switch (item) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    let ob = this.__ob__ 
    if (inserted) { 
      ob.observeArray(inserted) // 对我添加的对象进行劫持
    }
    return arr
  }
})

总结:

问题1: 在后续添加直接添加的属性不会是响应式的

答: vue2是用过Object.defineProperty实现数据响应式, 组件初始化时,对 data 中的 item 进行递归遍历,对 item 的每一个属性进行劫持,添加 set , get 方法。我们后来 新加的属性 ,并没有通过Object.defineProperty设置成响应式数据,修改后不会视图更新

问题2:数组/对象的响应式 ,vue 里面是怎么处理的?

答:

对象:使用了Object.defineProperty中的 get 和 set 实现响应式

数组: Vue重写了数组的原型,更准确的表达是拦截了数组的原型实现响应式

问题3:如需给后添加的属性做响应式

答:

Vue.set(target,propertyName/index,value) 或 vm.$set(target,propertyName/index,value)

如果是数组的话还可以直接使用新数组覆盖原始数组

问题4: 为什么对象和数组要分开来处理

答: 因为对象和数组的特性不同,处理它们的响应式需要不同的机制,数组使用重写方法是确保在调用这些方法时能够通知 Vue 进行响应式更新,因为直接修改数组的元素或长度 Vue 是无法追踪的。

对象的特性:

  1. 属性访问和设置: 对象使用键值对存储数据,可以通过点语法或中括号语法访问和设置属性。
  2. 可扩展性: 对象是动态的,可以随时添加或删除属性。
  3. 属性的定义性: 可以使用 Object.defineProperty 定义属性,并设置属性的特性(比如可枚举性、可配置性、可写性)。
  4. 枚举性: 对象的属性可以是可枚举的(可以通过 for...in 循环遍历到)或不可枚举的。

数组的特性:

  1. 有序集合: 数组是一种有序集合,它的元素是按照索引顺序存储的。
  2. 长度: 数组有一个 length 属性,可以动态改变数组的长度。
  3. 变异方法: 数组具有可以改变自身的变异方法(如 push, pop, shift, unshift 等),这些方法会改变数组的内容。
  4. 迭代方法: 数组提供了一些迭代方法(如 map, filter, reduce 等),用于对数组进行操作或遍历。