Loading

Vue2.x 的数据劫持

Object.defineProperty()

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

const object1 = {};

Object.defineProperty(object1, "property1", {
  value: 42,
  writable: false,
});

object1.property1 = 77; // throws an error in strict mode
console.log(object1.property1); // 42

语法

Object.defineProperty(obj, prop, descriptor);

参数

  • obj - 要定义属性的对象。
  • prop - 要定义或修改的属性的名称或 Symbol 。
  • descriptor - 要定义或修改的属性描述符。

描述符介绍

对象里目前存在的属性描述符有两种主要形式:数据描述符存取描述符数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。

这两种描述符都是对象。它们共享以下可选键值(默认值是指在使用 Object.defineProperty() 定义属性时的默认值):

  • configurable - 当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
  • enumerable - 当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。默认为 false。

数据描述符还具有以下可选键值:

  • value - 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
  • writable - 当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。默认为 false。

数据描述符还具有以下可选键值:

  • get - 属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的 this 并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。默认为 undefined。
  • set - 属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined。

注意:

  1. 如果一个描述符不具有 value、writable、get 和 set 中的任意一个键,那么它将被认为是一个数据描述符。

  2. 如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常。

  3. 这些选项不一定是自身属性,也要考虑继承来的属性。为了确认保留这些默认值,在设置之前,可能要冻结 Object.prototype,明确指定所有的选项,或者通过 Object.create(null) 将 proto (en-US) 属性指向 null。

返回值

被传递给函数的对象。

注意: 在 ES6 中,由于 Symbol 类型的特殊性,用 Symbol 类型的值来做对象的 key 与常规的定义或修改不同,而 Object.defineProperty 是定义 key 为 Symbol 的属性的方法之一。

Vue2.x 的数据劫持

/**
 * 响应式 - 数据变了会通知更新视图
 */
function updateView() {
  console.log("更新视图");
}

let oldArrayPrototype = Array.prototype;
let proto = Object.create(oldArrayPrototype);

// 重写部分能直接修改原数组的方法
["push", "shift", "pop", "unshift", "splice"].forEach((method) => {
  proto[method] = function () {
    // 改写方法,但是还是要调用老的方法实现功能
    updateView();
    oldArrayPrototype[method].call(this, ...arguments);
  };
});

/**
 * 声明一个观察者
 */
function observer(target) {
  if (typeof target !== "object" || target == null) {
    return target;
  }

  if (Array.isArray(target)) {
    // 拦截数组,给数组的原型方法
    target.__proto__ = proto;
  }

  for (const key in target) {
    if (target.hasOwnProperty(key)) {
      defineReactive(target, key, target[key]);
    }
  }
}

/**
 * 定义一个响应式方法
 */
function defineReactive(target, key, value) {
  observer(value);
  Object.defineProperty(target, key, {
    get() {
      console.log("get value===", value);
      return value;
    },
    set(newValue) {
      if (newValue !== value) {
        updateView();
        console.log("set value===", newValue);
        value = newValue;
      }
    },
  });
}

测试(以下贴出了所以的控制台打印信息,{}都是可以展开的,展开也会调用 get 方法)

let data = {
  name: "frank",
  age: 20,
  otherInfo: {
    sex: "male",
  },
  clothes: ["shirt", "pants", "shoes"],
  frends: [
    { name: "link", age: 22 },
    { name: "lottery", age: 19 },
  ],
};
// 开始观察
observer(data);
data.age += 1;
// get value=== 20
// 更新视图
// set value=== 21
data.otherInfo.sex = "m";
// get value=== {}
// 更新视图
// set value=== m
data.clothes.unshift("hat");
// get value=== (3) [(...), (...), (...)]
// get value=== shirt
// get value=== pants
// get value=== shoes
// 更新视图
// get value=== shoes
// get value=== pants
// 更新视图
// set value=== pants
// get value=== shirt
// 更新视图
// set value=== shirt
// 更新视图
// set value=== hat
data.clothes.shift();
// get value=== (4) [(...), (...), (...), "shoes"]
// get value=== hat
// get value=== shirt
// get value=== pants
// 更新视图
// get value=== hat
// get value=== shirt
// 更新视图
// set value=== shirt
// get value=== pants
// 更新视图
// set value=== pants
// 更新视图
// set value=== shoes
data.frends.push("json");
// get value=== (2) [(...), (...)]
// get value=== {}
// get value=== {}
// 更新视图
data.frends.pop();
// get value=== (3) [(...), (...), "json"]
// get value=== {}
// get value=== {}
// 更新视图
posted @ 2021-05-25 23:38  Frank-Link  阅读(188)  评论(0编辑  收藏  举报