logo

这下不得不看一下Proxy了

Sat Feb 11 2023 Posted 2 years ago

如果想要了解 Vue3 的响应式原理,Proxy 是不可绕过的一环,简单整(pin)理(cou)一篇博客来证明我确实知道有这么个东西(逃

Proxy #

定义 #

MDN 上给出的定义:Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

语法 #

const p = new Proxy(target, handler)

Proxy 具有两个参数:

  • target —— 需要包装的对象,可以是任何东西,包括函数。
  • handler —— 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

p 进行操作时,如果 handler 中存在响应的 traps,则会运行它,否则的话将直接对 target 进行原生的处理。

捕获器(traps) #

如果不设置任何捕获器(traps)的话,所有对 proxy 的操作都会被直接转发给 target

const target = {}
const proxy = new Proxy(target, {})

proxy.test = 5

console.log(target.test) // 5
console.log(proxy.test) // 5

for (const key in proxy) console.log(key) // test

但如果我们设置了捕获器,就可以捕获到对应的操作并且可以修改它的表现。

const person = {
  name: 'sechi',
}

const proxy = new Proxy(person, {
  get(target, property, receiver) {
    console.log(`正在访问${property}属性`)
    return target[property]
  },
})

console.log(proxy.name) // (1)正在访问name属性  (2)sechi
console.log(proxy.age) // (1)正在访问age属性  (2)undefined

算上 get 捕获器,Proxy 总共支持13种捕获器,如下:

内部方法Handler 方法何时触发
[[Get]]get读取属性
[[Set]]set写入属性
[[HasProperty]]hasin 操作符
[[Delete]]deletePropertydelete 操作符
[[Call]]apply函数调用
[[Construct]]constructnew 操作符
[[GetPrototypeOf]]getPrototypeOfObject.getPrototypeOf
[[SetPrototypeOf]]setPrototypeOfObject.setPrototypeOf
[[IsExtensible]]isExtensibleObject.isExtensible
[[PreventExtensions]]preventExtensionsObject.preventExtensions
[[DefineOwnProperty]]definePropertyObject.defineProperty, Object.defineProperties
[[GetOwnProperty]]getOwnPropertyDescriptorObject.getOwnPropertyDescriptor, for..in, Object.keys/values/entries
[[OwnPropertyKeys]]ownKeysObject.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object.keys/values/entries

其中比较常用的有:get, set, deleteProperty, ownKeys, in, apply

可撤销的Proxy #

Proxy.revocable() 可以创建一个可撤销的代理对象。

该方法会返回一个对象,其结构为 {"proxy": proxy, "revoke": revoke},其中 proxy 表示代理对象本身,和使用 new Proxy 创建的代理对象没有任何不同,只不过它可以通过 revoke() 方法被撤销掉。

const object = {
  data: 'Valuable data'
}

const { proxy, revoke } = Proxy.revocable(object, {})

console.log(proxy.data) // Valuable data

revoke()

console.log(proxy.data) // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked

Reflect #

Reflect 是一个内建对象,可简化 Proxy 的创建。它不是一个函数对象,因此它是不可构造的。

前面所讲过的内部方法,例如 [[Get]][[Set]] 等,都只是规范性的,不能直接调用。

Reflect 对象使调用这些内部方法成为了可能。它的方法是内部方法的最小包装。

它和 Proxy 一样,总共有13种方法:

操作Reflect 调用内部方法
obj[prop]Reflect.get(obj, prop)[[Get]]
obj[prop] = valueReflect.set(obj, prop, value)[[Set]]
delete obj[prop]Reflect.deleteProperty(obj, prop)[[Delete]]
new F(value)Reflect.construct(F, value)[[Construct]]
.........
const person = {}

Reflect.set(person, 'name', 'sechi')

alert(person.name) // sechi

我们可以简单地认为 Reflect 就是对象的原生内部行为的包装

const person = {
  name: 'sechi',
}

user = new Proxy(user, {
  get(target, prop, receiver) {
    alert(`GET ${prop}`)
    return Reflect.get(target, prop, receiver)
  },
  set(target, prop, val, receiver) {
    alert(`SET ${prop}=${val}`)
    return Reflect.set(target, prop, val, receiver)
  }
})

const name = person.name // GET name
person.name = 'mike' // SET name=mike

在大部分情况下,我们都可以将上方代码中的 Reflect.get(target, prop, receiver) 替换为 target[prop],但两者之间其实还是存在一些微小的差异:

const student = {
  _name: 'sechi',
  get name() {
    return this._name
  }
}

const studentProxy = new Proxy(student, {
  get(target, prop, receiver) {
    return target[prop]
  }
})

const teacher = {
  __proto__: studentProxy,
  _name: 'document'
}

console.log(teacher.name) // sechi

根据我们的直觉,上面的代码应该打印 document 而不是 sechi,出现这个问题的原因其实就是 Proxy

  1. 当我们读取 teacher.name 时,由于 teacher 对象自身没有对应的的属性,搜索将转到其原型。
  2. 其原型是 studentProxy
  3. 从代理读取 name 属性时,get 捕捉器会被触发,并从原始对象返回 target[prop] 属性,当调用 target[prop] 时,若 prop 是一个 getter,它将在 this=target 上下文中运行其代码。因此,结果是来自原始对象 targetthis._name,即来自 user

对于一个普通函数,我们可以使用 call/apply 来改变它的 this 指向,但如果是一个 getter 那就不行了,因为它不能被调用,只能被访问。为了解决这个问题,我们就需要使用 Reflect.get 了:

...
let studentProxy = new Proxy(student, {
  get(target, prop, receiver) { //  receiver可以保留正确的this指向 这里的receiver = teacher
    return Reflect.get(target, prop, receiver);
    // 或者可以简写为 Reflect.get(...arguments);
  }
});
...

由此我们可以发现 Reflect 提供了一种安全的方式来完成转发操作。

使用场景 #

这里直接把阿宝哥的文章中的示例复制过来了,写的非常全面且仔细。

增强型数组 #

定义 enhancedArray 函数
function enhancedArray(arr) {
  return new Proxy(arr, {
    get(target, property, receiver) {
      const range = getRange(property);
      const indices = range ? range : getIndices(property);
      const values = indices.map(function (index) {
        const key = index < 0 ? String(target.length + index) : index;
        return Reflect.get(target, key, receiver);
      });
      return values.length === 1 ? values[0] : values;
    },
  });

  function getRange(str) {
    var [start, end] = str.split(":").map(Number);
    if (typeof end === "undefined") return false;

    let range = [];
    for (let i = start; i < end; i++) {
      range = range.concat(i);
    }
    return range;
  }

  function getIndices(str) {
    return str.split(",").map(Number);
  }
}
使用 enhancedArray 函数
const arr = enhancedArray([1, 2, 3, 4, 5]);

console.log(arr[-1]); //=> 5
console.log(arr[[2, 4]]); //=> [ 3, 5 ]
console.log(arr[[2, -2, 1]]); //=> [ 3, 4, 2 ]
console.log(arr["2:4"]); //=> [ 3, 4]
console.log(arr["-2:3"]); //=> [ 4, 5, 1, 2, 3 ]

由以上的输出结果可知,增强后的数组对象,就可以支持负数索引、分片索引等功能。除了可以增强数组之外,我们也可以使用 Proxy API 来增强普通对象。

增强型对象 #

创建 enhancedObject 函数
const enhancedObject = (target) =>
  new Proxy(target, {
    get(target, property) {
      if (property in target) {
        return target[property];
      } else {
        return searchFor(property, target);
      }
    },
  });

let value = null;
function searchFor(property, target) {
  for (const key of Object.keys(target)) {
    if (typeof target[key] === "object") {
      searchFor(property, target[key]);
    } else if (typeof target[property] !== "undefined") {
      value = target[property];
      break;
    }
  }
  return value;
}
使用 enhancedObject 函数
const data = enhancedObject({
  user: {
    name: "阿宝哥",
    settings: {
      theme: "dark",
    },
  },
});

console.log(data.user.settings.theme); // dark
console.log(data.theme); // dark
复制代码

以上代码运行后,控制台会输出以下代码:

dark
dark

通过观察以上的输出结果可知,使用 enhancedObject 函数处理过的对象,我们就可以方便地访问普通对象内部的深层属性。

创建只读的对象 #

创建 Proxy 对象
const man = {
  name: "semlinker",
};

const handler = {
  set: "Read-Only",
  defineProperty: "Read-Only",
  deleteProperty: "Read-Only",
  preventExtensions: "Read-Only",
  setPrototypeOf: "Read-Only",
};

const proxy = new Proxy(man, handler);
使用 proxy 对象
console.log(proxy.name);
proxy.name = "kakuqo";

以上代码运行后,控制台会输出以下代码:

semlinker
proxy.name = "kakuqo";
           ^
TypeError: 'Read-Only' returned for property 'set' of object '#<Object>' is not a function
复制代码

观察以上的异常信息可知,导致异常的原因是因为 handler 对象的 set 属性值不是一个函数。如果不希望抛出运行时异常,我们可以定义一个 freeze 函数:

function freeze (obj) {
  return new Proxy(obj, {
    set () { return true; },
    deleteProperty () { return false; },
    defineProperty () { return true; },
    setPrototypeOf () { return true; }
  });
}

定义好 freeze 函数,我们使用数组对象来测试一下它的功能:

let frozen = freeze([1, 2, 3]);
frozen[0] = 6;
delete frozen[0];
frozen = Object.defineProperty(frozen, 0, { value: 66 });
console.log(frozen); // [ 1, 2, 3 ]

上述代码成功执行后,控制台会输出 [ 1, 2, 3 ],很明显经过 freeze 函数处理过的数组对象,已经被 “冻结” 了。

拦截方法调用 #

定义 traceMethodCalls 函数
function traceMethodCalls(obj) {
  const handler = {
    get(target, propKey, receiver) {
      const origMethod = target[propKey]; // 获取原始方法
      return function (...args) {
        const result = origMethod.apply(this, args);
        console.log(
          propKey + JSON.stringify(args) + " -> " + JSON.stringify(result)
        );
        return result;
      };
    },
  };
  return new Proxy(obj, handler);
}
使用 traceMethodCalls 函数
const obj = {
  multiply(x, y) {
    return x * y;
  },
};

const tracedObj = traceMethodCalls(obj);
tracedObj.multiply(2, 5); // multiply[2,5] -> 10

上述代码成功执行后,控制台会输出 multiply[2,5] -> 10,即我们能够成功跟踪 obj 对象中方法的调用过程。其实,除了能够跟踪方法的调用,我们也可以跟踪对象中属性的访问,具体示例如下:

function tracePropAccess(obj, propKeys) {
  const propKeySet = new Set(propKeys);
  return new Proxy(obj, {
    get(target, propKey, receiver) {
      if (propKeySet.has(propKey)) {
        console.log("GET " + propKey);
      }
      return Reflect.get(target, propKey, receiver);
    },
    set(target, propKey, value, receiver) {
      if (propKeySet.has(propKey)) {
        console.log("SET " + propKey + "=" + value);
      }
      return Reflect.set(target, propKey, value, receiver);
    },
  });
}

const man = {
  name: "semlinker",
};
const tracedMan = tracePropAccess(man, ["name"]);

console.log(tracedMan.name); // GET name; semlinker
console.log(tracedMan.age); // undefined
tracedMan.name = "kakuqo"; // SET name=kakuqo

在以上示例中,我们定义了一个 tracePropAccess 函数,该函数接收两个参数:obj 和 propKeys,它们分别表示需跟踪的目标和需跟踪的属性列表。调用 tracePropAccess 函数后,会返回一个代理对象,当我们访问被跟踪的属性时,控制台就会输出相应的访问日志。

隐藏属性 #

创建 hideProperty 函数
const hideProperty = (target, prefix = "_") =>
  new Proxy(target, {
    has: (obj, prop) => !prop.startsWith(prefix) && prop in obj,
    ownKeys: (obj) =>
      Reflect.ownKeys(obj).filter(
        (prop) => typeof prop !== "string" || !prop.startsWith(prefix)
      ),
    get: (obj, prop, rec) => (prop in rec ? obj[prop] : undefined),
  });
使用 hideProperty 函数
const man = hideProperty({
  name: "阿宝哥",
  _pwd: "www.semlinker.com",
});

console.log(man._pwd); // undefined
console.log('_pwd' in man); // false
console.log(Object.keys(man)); // [ 'name' ]

通过观察以上的输出结果,我们可以知道,利用 Proxy API,我们实现了指定前缀属性的隐藏。除了能实现隐藏属性之外,利用 Proxy API,我们还可以实现验证属性值的功能。

验证属性值 #

创建 validatedUser 函数
const validatedUser = (target) =>
  new Proxy(target, {
    set(target, property, value) {
      switch (property) {
        case "email":
          const regex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
          if (!regex.test(value)) {
            console.error("The user must have a valid email");
            return false;
          }
          break;
        case "age":
          if (value < 20 || value > 80) {
            console.error("A user's age must be between 20 and 80");
            return false;
          }
          break;
      }

      return Reflect.set(...arguments);
    },
  });
使用 validatedUser 函数
let user = {
  email: "",
  age: 0,
};

user = validatedUser(user);
user.email = "semlinker.com"; // The user must have a valid email
user.age = 100; // A user's age must be between 20 and 80

上述代码成功执行后,控制台会输出以下结果:

The user must have a valid email
A user's age must be between 20 and 80

tips #

深度监听问题 #

Proxy 没有深层监听,如果想实现深度监听功能需要进行递归

const person = {
  name: 'sechi',
  age: 23,
  children: {
    name: 'null'
  }
}
const proxy = new Proxy(person, {
  get(obj, key) {
    console.log('触发了get')
    return key in obj ? obj[key] : '404'
  },
  set(obj, key, val) {
    console.log('触发了set')
    obj[key] = val
    return true
  }
})

console.log(proxy.children.name)// 触发了get null
console.log(proxy.children.height)// 触发了get undefined ****

proxy.children.name = '777' // 触发了get
console.log(proxy.children.name)// 触发了get 777

proxy.children.height 打印出了undefined 的原因:Proxy 并没有进行深度监听,所以当我们访问 proxy.children.height 时,由于 proxy 是具有 children 属性的,所以 obj[key] 会返回 true,相当于我们访问的是 person.children.height,自然也就返回了 undefined

this指向问题 #

const target = {
  checkThis() {
    console.log(this === proxyObj)
  }
}
const handler = {}
let proxyObj = new Proxy(target, handler)

proxyObj.checkThis()// true
target.checkThis()// false
const _name = new WeakMap()
class Person {
  constructor(name) {
    _name.set(this, name)
  }

  get name() {
    return _name.get(this)
  }
}

const sechi = new Person('sechi')
jane.name // 'sechi'

const proxyObj = new Proxy(sechi, {})
proxyObj.name // undefined

上面代码取不到 proxyObj.name 的原因是 name 属性的获取依靠 this 的指向,而 proxyObjthis 指向自身,所以导致了无法正常代理。

receiver参数 #

receiver 指向当前 Proxy 对象或者继承于当前 Proxy 对象

const proxy = new Proxy({},
  {
    get(target, property, receiver) {
      return receiver
    },
  }
)

console.log(proxy.getReceiver === proxy) // true
const inherits = Object.create(proxy)
console.log(inherits.getReceiver === inherits) // true

我们可以通过 Reflet 来改变 receiver 的指向:

const obj = {
  get foo() {
    return this.bar
  },
}

console.log(Reflect.get(obj, 'foo')) // undefined
console.log(Reflect.get(obj, 'foo', { bar: 777 })) // 777

参考资料

  1. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
  2. https://zh.javascript.info/proxy
  3. https://juejin.cn/post/6924442692667572237
  4. https://juejin.cn/post/7069397770766909476