当前位置: 首页 > article >正文

vue2.x中的数据劫持

数据劫持(Data Hijacking)

  1. 数据劫持是一种核心技术,用于监听数据的变化,然后根据这些变化触发相应的操作。
  2. 在 Vue 中,数据劫持 是一种通过拦截对数据的访问和修改,从而实现数据绑定和响应式更新的技术。Vue 利用这一机制,使得当数据发生变化时,视图能够自动更新,从而实现 MVVM(Model-View-ViewModel)架构的双向绑定。

Vue2.x Object 的数据劫持

Object的数据劫持步骤

  1. 使用 Object.defineProperty()
    • 目的:定义或修改对象的属性。
    • 做法:Vue 利用 Object.defineProperty() 来定义(或修改)对象的属性,这样就可以在属性被访问或修改时,执行一些额外的逻辑(如通知更新)。
    • 示例:
let obj = { foo: 'bar' };
Object.defineProperty(obj, 'foo', {
  configurable: true, // 是否可以配置(修改属性 descriptor)
  enumerable: true,   // 是否可以枚举(for...in、Object.keys())
  get: function() {   // 获取属性时调用
    console.log('获取 foo 属性');
    return 'bar';
  },
  set: function(newValue) { // 设置属性时调用
    console.log('更新 foo 属性为:' + newValue);
    // 在这里,Vue 会进行依赖通知,触发更新
  }
});
  1. 递归劫持
    • 目的:确保对象的所有嵌套属性也被劫持。
    • 做法:Vue会递归遍历对象的所有属性,如果该属性的值也是对象,则对其也进行数据劫持。这确保了无论对象嵌套多深,数据变化都能被检测到。

Vue 2.x 源码解析

文件:vue/src/core/observer/index.js (Vue 2.6.12)
关键源码片段

/**
 * 观察者(Observer)构造函数
 * @param {Object} value - 需要被观察的对象
 */
export class Observer {
  constructor(value) {
    this.value = value; // 被观察对象
    this.dep = new Dep(); // 依赖管理器
    this.vmCount = 0; // 统计被多少个 Vue 实例使用

    // 定义不可枚举的 __ob__ 属性,指向当前 Observer 实例
    def(value, '__ob__', this);

    // 如果是数组,走数组处理逻辑
    if (Array.isArray(value)) {
      // ...
    } else {
      // 递归定义对象属性
      this.walk(value);
    }
  }

  /**
   * 递归定义对象属性
   * @param {Object} obj - 被观察对象
   */
  walk(obj) {
    const keys = Object.keys(obj); // 获取对象键数组
    for (let i = 0; i < keys.length; i++) {
      // 定义响应式属性
      defineReactive(obj, keys[i]);
    }
  }
}

/**
 * 定义响应式属性
 * @param {Object} obj - 被观察对象
 * @param {string} key - 属性键
 * @param {any} val - 属性值
 */
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep(); // 依赖管理器

  // 递归观察子对象
  let childOb = !shallow && observe(val);

  // 使用 Object.defineProperty 定义属性
  Object.defineProperty(obj, key, {
    enumerable: true, // 可枚举
    configurable: true, // 可配置
    get: function reactiveGetter() {
      // 获取属性时
      const value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        // 依赖收集
        dep.depend();
        if (childOb) {
          dependArray(value);
        }
      }
      return value;
    },
    set: function reactiveSetter(newVal) {
      // 设置属性时
      const value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return;
      }
      /* eslint-enable no-self-compare */
      if (customSetter) {
        customSetter();
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      // 新值观察
      childOb = !shallow && observe(newVal);
      // 通知更新
      dep.notify();
    }
  });
}
Observer 类
  1. 构造函数
    • value: 被观察的对象。
    • dep: 依赖管理器,用于管理订阅该对象的依赖。
    • vmCount: 统计被多少个 Vue 实例使用。
    • def(value, ‘_ob_’, this): 定义不可枚举的 _ob_ 属性,指向当前 Observer 实例,以便在其他地方可以访问到该实例。
    • 如果 value 是数组,则会处理数组的响应式逻辑(此处未实现)。
    • 如果 value 是对象,则调用 walk 方法递归地定义对象的每个属性的响应式特性。
  2. walk 方法:
    • 获取对象的所有键,然后遍历这些键,调用 defineReactive 方法为每个属性定义响应式特性。
defineReactive 函数
  1. 参数:
    • obj: 被观察的对象。
    • key: 对象的属性键。
    • val: 属性值。
    • customSetter: 自定义的 setter 函数。
    • shallow: 是否浅层观察,如果不为真,则递归观察子对象。
  2. 依赖管理:
    • 创建一个 dep 实例用于管理该属性的依赖。
    • 如果属性值是一个对象且不是浅层观察,则递归调用 observe 函数来观察子对象。
  3. 属性描述符:
    • 使用 Object.defineProperty 来重新定义对象的属性,使其具备响应式特性。
    • getter:当属性被读取时,收集依赖。如果属性值是一个对象,则继续收集子对象的依赖。
    • setter:当属性被设置时,如果新值和旧值不同,则更新值,并通知所有依赖进行更新。
依赖收集和通知
  1. 依赖收集:当属性被读取时,如果存在 Dep.target(表示有依赖正在收集),则调用 dep.depend() 进行依赖收集,同时对子对象进行依赖收集。
  2. 通知更新:当属性被设置时,通知所有订阅该属性的依赖进行更新,调用 dep.notify()。

Vue2.x Array 的数据劫持

由于Array也是对象的一种(特殊的对象),理论上可以使用Object.defineProperty()来劫持。但是,数组有一些特殊的方法(如push、pop、splice等),直接使用defineProperty劫持可能不足以捕获所有变化。

Array劫持策略

  1. 重写数组原型方法
    • 目的:拦截数组方法调用,以检测变化并通知更新。
    • 做法:Vue 重写了数组的原型方法(如push、pop、shift、unshift、splice、sort、reverse),在这些方法内部,除了执行原有的逻辑外,还会主动触发依赖更新。
  2. 使用 Object.defineProperty() 劫持数组长度
    • 目的:监听数组长度的变化,因为数组长度的变化可能是通过索引直接修改数组元素导致的。
    • 做法:通过Object.defineProperty()劫持数组的length属性,以捕获通过索引修改数组长度的情况。

文件:vue-next/src/reactivity/reactive.ts
关键源码片段

// 缓存原始数组原型方法
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);

// 重写数组原型方法
[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
].forEach(function(method) {
  // 缓存原始方法
  const original = arrayProto[method];
  // 定义新的方法
  def(arrayMethods, method, function mutator(...args) {
    // 调用原始方法
    const result = original.apply(this, args);
    const ob = this.__ob__;
    let inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args;
        break;
      case 'splice':
        inserted = args.slice(2);
        break;
    }
    if (inserted) ob.observeArray(inserted);
    // 通知更新
    ob.dep.notify();
    return result;
  });
});
  1. 缓存原始数组原型方法

    • arrayProto:引用了 JavaScript 的原生数组原型 Array.prototype,这是为了保留数组的原始方法,不直接修改它。
    • arrayMethods:创建了一个以 Array.prototype 为原型的新对象,用来定义自定义的数组方法。通过 Object.create(),arrayMethods 继承了 Array.prototype 的所有方法。
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
  1. 重写数组原型方法
    列出常见的可以修改数组内容的方法。包括:
    • push:向数组末尾添加元素。
    • pop:从数组末尾移除元素。
    • shift:从数组开头移除元素。
    • unshift:向数组开头添加元素。
    • splice:添加或移除数组中的元素。
    • sort:对数组进行排序。
    • reverse:反转数组的顺序。
[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
].forEach(function(method) {
  1. 缓存原始方法并定义新的方法
    • original:保存了对应的数组方法的原始实现,比如 push、pop 等。
    • def():假设这是一个用于定义属性的工具函数,可能类似于 Object.defineProperty,它将新的自定义方法定义在 arrayMethods 对象上。
    • mutator(…args):这是一个新的方法,它会替代原来的数组方法。当你在数组上调用 push 等方法时,实际上调用的是这个 mutator 函数。
    • original.apply(this, args):在新的方法中,仍然会调用原始的数组方法,并将 this 和参数传递进去。这保证了数组的原始行为不会被改变。
  2. 处理新插入的元素
    • ob:假设 this 是一个被观察的数组实例,而 _ob_ 是该数组的观察者对象。这个对象通常包含了观察功能,比如响应式数据系统中的 Observer 实例。
    • inserted:用于保存新插入到数组中的元素。push 和 unshift 方法会直接把所有传入的参数当作新元素;splice 则从第三个参数开始表示新插入的元素(args.slice(2))。
    • ob.observeArray(inserted):如果有新插入的元素,调用 observeArray 方法对这些新元素进行观察。这个过程通常会递归地将新元素也变成响应式的,确保数组中的每个元素都能被追踪变化。
const ob = this.__ob__;
let inserted;
switch (method) {
  case 'push':
  case 'unshift':
    inserted = args;
    break;
  case 'splice':
    inserted = args.slice(2);
    break;
}
if (inserted) ob.observeArray(inserted);
  1. 通知依赖更新

    • ob.dep.notify():通知依赖于这个数组的其他部分(比如视图层)发生了变化。这个 dep 通常是依赖管理系统中的一个实例,用于触发依赖的重新计算或视图的重新渲染。
    • return result:最后返回原始方法的执行结果,保证方法的正常功能不受影响。
  ob.dep.notify();
  return result;

总结

  1. Vue 2.x:
    • Object劫持:主要通过Object.defineProperty()递归定义(或修改)对象属性,用于捕获属性的读写操作。
    • Array劫持:采用了重写数组原型方法和定义length属性的方式,以确保捕获到数组所有可能的变化操作。

http://www.kler.cn/news/363806.html

相关文章:

  • 当数据遇到威胁:强化治理以抵御网络攻击
  • python离线安装依赖
  • c++实现跳表
  • [论文笔记] llama-factory 微调qwen2.5、llama3踩坑
  • 21、基于Firefly-rk3399字符设备驱动寄存器控制LED
  • Maven入门到实践:从安装到项目构建与IDEA集成
  • 视频剪辑和转换gif一体化UI页面【可以解决gif体积过大】
  • 【YOLOv11】制作使用YOLOv11的docker环境
  • 一道面试题:为什么要使用Docker?
  • Java项目-基于springboot框架的智慧外贸系统项目实战(附源码+文档)
  • COVON全意卫生巾凭借其轻薄、透气、绵柔的特点,在东南亚市场上迅速走红
  • 攻坚金融关键业务系统,OceanBase亮相2024金融科技大会
  • 调整Android板子的分辨率
  • 内网python smtplib用ssh隧道通过跳板机发邮件
  • 微积分复习笔记 Calculus Volume 1 - 3.2 he Derivative as a Function
  • 【Linux学习】(3)Linux的基本指令操作
  • 关闭或开启Win11系统的自动更新
  • springboot接口Get请求实体类入参
  • VirtualBox、VMware安装Linux后重启再次进入安装界面的问题解决
  • 微信小程序用开发工具在本地真机调试可以正常访问摄像头,发布了授权后却无法访问摄像头,解决方案
  • 钡铼技术R10工业4G路由服务智慧城市建设
  • Tauri(读音:踹你)开发简介:1.创建和运行第一个app
  • 20241021给荣品RD-RK3588-AHD开发板刷荣品预编译的Android12之后使用GPStest测试板载GPS
  • 深度学习:pandas篇
  • 【小白学机器学习21】 理解假设检验的关键:反证法
  • Android组件化开发