欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 培训 > vue2.x中的数据劫持

vue2.x中的数据劫持

2024/11/30 12:42:32 来源:https://blog.csdn.net/weixin_46074818/article/details/143162725  浏览:    关键词: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属性的方式,以确保捕获到数组所有可能的变化操作。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com