Так называемый прокси-сервер данных (также называемый перехватом данных) относится к перехвату этого поведения с помощью фрагмента кода при доступе или изменении атрибута объекта, выполнении дополнительных операций или изменении возвращаемого результата. Это более характерноObject.defineProperty()и новое в ES2015Proxyобъект. Есть и заброшенныеObject.observe(), причина отказа именноProxy, поэтому мы не будем продолжать здесь обсуждать этот метод, который был удален браузером.
Наиболее известным применением перехвата данных является двусторонняя привязка, которая также является обязательным вопросом на собеседовании, который плохо обсуждался. Например, Vue 2.x используетObject.defineProperty()(Vue переключился на версию 3.xProxyдля реализации). Кроме того, immer.js использует неизменное свойство данных, чтобы гарантироватьProxyЧтобы заблокировать регулярные операции модификации, это также приложение для захвата данных.
Давайте взглянем на плюсы и минусы этих двух методов.
Object.defineProperty
Двусторонняя привязка Vue была повышена до обязательного вопроса для фронтенд-интервью, я не буду повторять принцип. Проще говоря, используйтеObject.defineProperty(), и отделите внутренние компоненты от Observer, Dep и используйте Watcher для подключения.
Object.defineProperty()Есть три основные проблемы:
Не могу отслеживать изменения массива
См. следующий код:
let arr = [1,2,3]
let obj = {}
Object.defineProperty(obj, 'arr', {
get () {
console.log('get arr')
return arr
},
set (newVal) {
console.log('set', newVal)
arr = newVal
}
})
obj.arr.push(4) // 只会打印 get arr, 不会打印 set
obj.arr = [1,2,3,4] // 这个能正常 set
Следующие методы массива не будут запускатьсяset:
pushpopshiftunshiftsplicesortreverse
Vue определяет эти методы как методы мутации, то есть методы, изменяющие исходный массив. Аналогом является немутирующий метод, например.filter, concat, sliceи т. д., ни один из них не изменит исходный массив, а вернет новый массив. На официальном сайте Vue естьСвязанная документацияРасскажите этот вопрос.
Подход Vue заключается в переопределении этих методов для реализации захвата массива. Минималистская реализация выглядит следующим образом:
const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
const arrayAugmentations = {};
aryMethods.forEach((method)=> {
// 这里是原生 Array 的原型方法
let original = Array.prototype[method];
// 将 push, pop 等封装好的方法定义在对象 arrayAugmentations 的属性上
// 注意:是实例属性而非原型属性
arrayAugmentations[method] = function () {
console.log('我被改变啦!');
// 调用对应的原生方法并返回结果
return original.apply(this, arguments);
};
});
let list = ['a', 'b', 'c'];
// 将我们要监听的数组的原型指针指向上面定义的空数组对象
// 这样就能在调用 push, pop 这些方法时走进我们刚定义的方法,多了一句 console.log
list.__proto__ = arrayAugmentations;
list.push('d'); // 我被改变啦!
// 这个 list2 是个普通的数组,所以调用 push 不会走到我们的方法里面。
let list2 = ['a', 'b', 'c'];
list2.push('d'); // 不输出内容
Каждое свойство объекта должно быть пройдено
использоватьObject.defineProperty()Наиболее сотрудничатьObject.keys()И обход, так что есть еще один слой вложенности. Такие как:
Object.keys(obj).forEach(key => {
Object.defineProperty(obj, key, {
// ...
})
})
Вложенные объекты должны быть глубоко пройдены
Так называемый вложенный объект относится к чему-то вроде
let obj = {
info: {
name: 'eason'
}
}
Если это вложенный объект этого типа, то его необходимо обходить слой за слоем, пока не будет вызвано каждое свойство каждого объекта.Object.defineProperty()до. Такую логику можно найти в исходном коде Vue (называетсяwalkметод).
Proxy
ProxyОн был официально добавлен в спецификацию ES2015, хотя его поддержка не так хороша, какObject.defineProperty(), но в основном поддерживается (за исключением нескольких браузеров, таких как IE и Opera Mini, данные поступают изcaniuse), так что это не слишком проблематично использовать.
объект
Что касается проблемы захвата данных,Proxyможно считатьObject.defineProperty()обновленная версия. Доступ к объекту из внешнего мира должен проходить через этот уровень перехвата. Поэтому это длявесь объект, вместоатрибут объекта, так что не надоkeysтраверс. Это решает вышеуказанноеObject.defineProperty()второй вопрос.
let obj = {
name: 'Eason',
age: 30
}
let handler = {
get (target, key, receiver) {
console.log('get', key)
return Reflect.get(target, key, receiver)
},
set (target, key, value, receiver) {
console.log('set', key, value)
return Reflect.set(target, key, value, receiver)
}
}
let proxy = new Proxy(obj, handler)
proxy.name = 'Zoe' // set name Zoe
proxy.age = 18 // set age 18
Как и выше код,Proxyнаправлен наobjиз. Поэтому неважноobjСколько ключей содержится внутри, можно ввестиset. (сохранил одинObject.keys()обход)
Также этоReflect.getа такжеReflect.setМожно понимать как наследование классовsuper, который вызывает исходный метод. Детальное отражение можно посмотретьздесь, эта статья не расширяется.
поддерживаются массивы
let arr = [1,2,3]
let proxy = new Proxy(arr, {
get (target, key, receiver) {
console.log('get', key)
return Reflect.get(target, key, receiver)
},
set (target, key, value, receiver) {
console.log('set', key, value)
return Reflect.set(target, key, value, receiver)
}
})
proxy.push(4)
// 能够打印出很多内容
// get push (寻找 proxy.push 方法)
// get length (获取当前的 length)
// set 3 4 (设置 proxy[3] = 4)
// set length 4 (设置 proxy.length = 4)
ProxyНет необходимости перегружать методы массива, что избавляет от многих хаков, уменьшает количество кода и снижает стоимость обслуживания, а стандарт лучше.
Поддержка вложения
По сути,ProxyВложенность также не поддерживается, это иObject.defineProperty()это то же самое. Поэтому ее тоже нужно решать послойным обходом.Proxyнаписано наgetвнутренний рекурсивный вызовProxyи обратно, код выглядит следующим образом:
let obj = {
info: {
name: 'eason',
blogs: ['webpack', 'babel', 'cache']
}
}
let handler = {
get (target, key, receiver) {
console.log('get', key)
// 递归创建并返回
if (typeof target[key] === 'object' && target[key] !== null) {
return new Proxy(target[key], handler)
}
return Reflect.get(target, key, receiver)
},
set (target, key, value, receiver) {
console.log('set', key, value)
return Reflect.set(target, key, value, receiver)
}
}
let proxy = new Proxy(obj, handler)
// 以下两句都能够进入 set
proxy.info.name = 'Zoe'
proxy.info.blogs.push('proxy')
другие отличия
Кроме двух вышеуказанных моментов,ProxyОн также имеет следующие преимущества:
-
ProxyВторой параметр может иметь 13 методов перехвата, что болееObject.defineProperty()быть богаче -
ProxyВ качестве нового стандарта он сфокусирован и оптимизирован для производительности производителями браузеров по сравнению сObject.defineProperty()Это старый проверенный метод.
Это второе преимущество связано с тем, что это новый стандарт. Но у нового стандарта есть и недостатки, а именно:
-
Proxyне так совместим, какObject.defineProperty()(данные caniuse показывают, что браузер QQ и браузер Baidu не поддерживаютProxy, что неприемлемо для отечественной мобильной разработки, но оба поддерживаютObject.defineProperty()) - Невозможно использовать полифилл для обеспечения совместимости
Эти сравнения предназначены только для требования «реализации захвата данных».Object.defineProperty()помимо определенияgetа такжеsetКроме того, могут быть реализованы и другие функции, поэтому даже без учета совместимости эта статья не означает, что одна может полностью исключить другую.
заявление
Говорить о самой технологии, не говоря о сценариях применения, — это вообще хулиганство. Технология действительно ценна только тогда, когда у нее есть сценарий применения.
Как упоминалось в начале, захват данных в основном происходит в рамках таких фреймворков, как Vue, immer и т. д., но они, похоже, далеки от наших обычных программистов. Помимо этого, я перечислю несколько возможных сценариев применения, и вы, возможно, сможете придумать больше в своей обычной работе.
вопрос интервью
На самом деле, помимо чтения исходного кода привязки данных Vue, второй раз я узнал об этой технологии из странной темы, которая когда-то была популярна среди разработчиков:
как это
aВстречаться(a === 1 && a === 2 && a === 3) === trueШерстяная ткань? (обратите внимание, что их 3=, то есть строго равны)
Поскольку оно строго равно, преобразование типов в основном игнорируется. Естественно думать, что каждый визитaВозвращаемые значения разные, поэтому о перехвате данных обязательно будут думать. (Могут быть и другие решения, но здесь только способ угона данных)
let current = 0
Object.defineProperty(window, 'a', {
get () {
current++
return current
}
})
console.log(a === 1 && a === 2 && a === 3) // true
использоватьProxyДа, но потому чтоProxyСинтаксис должен вернуть новый объект, так чтоa === 1может быть сложнее сделатьobj.a === 1В любом случае, все в порядке, принцип тот же, и вам не нужно слишком беспокоиться.
множественное наследование
Javascript реализует наследование через цепочку прототипов.Обычно объект (или класс) может наследовать только один объект (или класс). Но с помощью этих двух методов можно реализовать своего рода черную технологию, позволяющую одному объекту наследовать два объекта. В следующем примере используетсяProxyвыполнить.
let foo = {
foo () {
console.log('foo')
}
}
let bar = {
bar () {
console.log('bar')
}
}
// 正常状态下,对象只能继承一个对象,要么有 foo(),要么有 bar()
let sonOfFoo = Object.create(foo);
sonOfFoo.foo(); // foo
let sonOfBar = Object.create(bar);
sonOfBar.bar(); // bar
// 黑科技开始
let sonOfFooBar = new Proxy({}, {
get (target, key) {
return target[key] || foo[key] || bar[key];
}
})
// 我们创造了一个对象同时继承了两个对象,foo() 和 bar() 同时拥有
sonOfFooBar.foo(); // foo 有foo方法,继承自对象foo
sonOfFooBar.bar(); // bar 也有bar方法,继承自对象bar
Конечно, я не думал о фактическом использовании, и, учитывая удобочитаемость кода, большинство из них могут существовать только в ослепительных навыках или вопросах на собеседовании, я думаю...
скрыть приватные переменные
Так как мы можем манипулировать get, мы можем, естественно, понять, что к некоторым свойствам можно получить доступ, а к некоторым нет Это концепция общедоступных и частных свойств. Это также очень просто реализовать:
function getObject(rawObj, privateKeys) {
return new Proxy(rawObj, {
get (target, key, receiver) {
if (privateKeys.indexOf(key) !== -1) {
throw new ReferenceError(`${key} 是私有属性,不能访问。`)
}
return target[key]
}
})
}
let rawObj = {
name: 'Zoe',
age: 18,
isFemale: true
}
let obj = getObject(rawObj, ['age'])
console.log(obj.name) // Zoe
console.log(obj.age) // 报错
Проверка при установке свойств объекта
Если некоторые свойства объекта имеют требования к типу и могут принимать значения только определенного типа, передатьProxyВместо рекурсивного обхода и единообразной проверки при его использовании мы можем выдать ошибку при его настройке. Это лучше как с точки зрения эффективности выполнения, так и с точки зрения удобства использования.
let person = {
name: 'Eason',
age: 30
}
let handler = {
set (target, key, value, receiver) {
if (key === 'name' && typeof value !== 'string') {
throw new Error('用户姓名必须是字符串类型')
}
if (key === 'age' && typeof value !== 'number') {
throw new Error('用户年龄必须是数字类型')
}
return Reflect.set(target, key, value, receiver)
}
}
let personForUser = new Proxy(person, handler)
personForUser.name = 'Zoe' // OK
personForUser.age = '18' // 报错
Различные проверки отказоустойчивости
Мы часто отправляем запросы на бэкенд, ждем ответа и обрабатываем данные ответа, а по надежности кода обычно много суждений, например:
// 发送请求代码省略,总之获取到了 response 对象了。
if (!response.data) {
console.log('响应体没有信息')
return
} else if (!response.data.message) {
console.log('后端没有返回信息')
return
} else if (!response.data.message.from || !response.data.message.text) {
console.log('后端返回的信息不完整')
return
} else {
console.log(`你收到了来自 ${response.data.message.from} 的信息:${response.data.message.text}`)
}
Суть кода в том, чтобы получитьresponse.data.message.fromа такжеresponse.data.message.text, но его нужно оценивать слой за слоем, иначе JS сообщит об ошибке.
мы можем рассмотреть возможность использованияProxyДавайте изменим этот код, чтобы он выглядел немного лучше.
// 故意设置一个错误的 data1,即 response.data = undefined
let response = {
data1: {
message: {
from: 'Eason',
text: 'Hello'
}
}
}
// 也可以根据 key 的不同给出更友好的提示
let dealError = key => console.log('Error key', key)
let isOK = obj => !obj['HAS_ERROR']
let handler = {
get (target, key, receiver) {
// 基本类型直接返回
if (target[key] !== undefined && typeof target[key] !== 'object') {
return Reflect.get(target, key, receiver)
}
// 如果是 undefined,把访问的的 key 传递到错误处理函数 dealError 里面
if (!target[key]) {
if (!target['HAS_ERROR']) {
dealError(key)
}
return new Proxy({HAS_ERROR: true}, handler)
}
// 正常的话递归创建 Proxy
return new Proxy(target[key], handler)
}
}
let resp = new Proxy(response, handler)
if (isOK(resp.data.message.text) && isOK(resp.data.message.from)) {
console.log(`你收到了来自 ${response.data.message.from} 的信息:${response.data.message.text}`)
}
потому что мы намеренно установилиresponse.data = undefined, таким образом входяdealErrorметод, параметрkeyценностьdata.
Хотя количество кода больше, чем указано вышеifПроверка дольше, ноisOK, handlerа такжеnew ProxyВсе определения можно использовать повторно, и их можно переместить в отдельный файл, открыв только несколько методов. Таким образом, фактический код толькоdealErrorОпределение и последнееifВот и все.
Больше сценариев применения
-
установить значение объекта по умолчанию- Создать объект со значениями по умолчанию для некоторых его свойств.
-
Оптимизированный тип перечисления- При сбое ключа типа перечисления немедленно сообщается об ошибке, а не возвращается молча
undefined, перезапись, удаление и т. д., вызванные ошибками написания кода, также могут быть перехвачены. -
Отслеживание изменений объектов и массивов- бросать событие при изменении элемента массива и объекта/свойства. Это может относиться к отмене, повтору или непосредственному возврату к истории состояния.
-
Добавьте кеш для доступа к свойствам объекта, чтобы повысить скорость- Запишите значение, когда установлено свойство объекта, и верните его напрямую, не обращаясь к свойству при доступе к нему. Добавлен механизм проверки TTL (Time To Live, время жизни) для предотвращения утечек памяти.
-
служба поддержки
inмассив ключевых слов- установивhasметод, вызываемый внутриarray.includes. При прямом использованииconsole.log('key' in someArr). -
Реализовать одноэлементный шаблон- установив
constructметод, выполнениеnewОператор всегда возвращает один и тот же синглтон, тем самым реализуя шаблон синглтона. -
Преобразование типов файлов cookie -
document.cookieэто использование;разделенная строка. Мы можем преобразовать его в объект и передатьProxyизsetа такжеdeletePropertyПереопределите настройки и удалите операции, чтобы предоставить работающий объект cookie для удобства использования.